slicejs-cli 3.6.3 → 3.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2525 +1,2525 @@
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
- // Emit in topological order so a module is registered before any module
1253
- // that depends on it (its transitive imports resolve at IIFE-eval time).
1254
- const filteredModules = this.sortDependencyModulesTopologically(
1255
- modules.filter((module) => !omittedDependencies.has(module.name))
1256
- );
1257
-
1258
- const lines = [
1259
- 'const SLICE_BUNDLE_DEPENDENCIES = {};',
1260
- ...this.getDefaultExportResolverLines()
1261
- ];
1262
- if (options.includeSharedResolver) {
1263
- lines.push(...this.getBundleDependencyResolverLines());
1264
- }
1265
- filteredModules.forEach((module, index) => {
1266
- const exportVar = `__sliceDepExports${index}`;
1267
- // Evaluate each dependency inside its own IIFE so its private,
1268
- // non-exported top-level bindings stay local and cannot collide with
1269
- // another dependency's (or the bundle's) identifiers. Only the exports
1270
- // object escapes the closure.
1271
- const transformedContent = this.transformDependencyContent(module.content, '__sliceExports', module.name);
1272
- // Bind this module's own (transitive) imports inside its IIFE — they were
1273
- // registered by earlier modules in the topological order.
1274
- const importBindings = this.buildDependencyBindings(
1275
- Object.fromEntries(
1276
- (module.moduleImports || [])
1277
- .filter((mi) => mi.bindings && mi.bindings.length)
1278
- .map((mi) => [mi.depName, { bindings: mi.bindings }])
1279
- ),
1280
- { preferShared: !!options.includeSharedResolver }
1281
- );
1282
- const body = transformedContent.trim();
1283
- lines.push(`const ${exportVar} = (() => {`);
1284
- lines.push('const __sliceExports = {};');
1285
- if (importBindings) lines.push(importBindings);
1286
- if (body) lines.push(body);
1287
- lines.push('return __sliceExports;');
1288
- lines.push('})();');
1289
- lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
1290
- });
1291
-
1292
- return lines.join('\n');
1293
- }
1294
-
1295
- sortDependencyModulesTopologically(modules = []) {
1296
- const byName = new Map(modules.map((module) => [module.name, module]));
1297
- const visited = new Set();
1298
- const ordered = [];
1299
- const visit = (module, stack) => {
1300
- if (visited.has(module.name) || stack.has(module.name)) return;
1301
- stack.add(module.name);
1302
- for (const imp of module.moduleImports || []) {
1303
- const dependency = byName.get(imp.depName);
1304
- if (dependency) visit(dependency, stack);
1305
- }
1306
- stack.delete(module.name);
1307
- visited.add(module.name);
1308
- ordered.push(module);
1309
- };
1310
- for (const module of modules) visit(module, new Set());
1311
- return ordered;
1312
- }
1313
-
1314
- async buildDependencyContents(jsContent, componentPath) {
1315
- const dependencyContents = {};
1316
- const visited = new Set();
1317
-
1318
- // Recursively resolve the relative-import graph rooted at `content`, so a
1319
- // dependency module's OWN (transitive) imports are inlined too. Returns the
1320
- // consumer's direct imports as [{ depName, bindings }].
1321
- const resolveModule = async (content, basePath) => {
1322
- const consumerImports = [];
1323
-
1324
- for (const dep of this.analyzeDependencies(content, basePath)) {
1325
- const depName = path.relative(this.srcPath, dep.path).replace(/\\/g, '/');
1326
- consumerImports.push({ depName, bindings: dep.bindings || [] });
1327
-
1328
- if (visited.has(depName)) continue;
1329
- visited.add(depName);
1330
-
1331
- try {
1332
- const depContent = await fs.readFile(dep.path, 'utf-8');
1333
- // Resolve this module's own transitive imports first.
1334
- const moduleImports = await resolveModule(depContent, path.dirname(dep.path));
1335
- dependencyContents[depName] = { content: depContent, bindings: [], moduleImports };
1336
- } catch (error) {
1337
- console.warn(`Warning: Could not read dependency ${dep.path}:`, error.message);
1338
- }
1339
- }
1340
-
1341
- return consumerImports;
1342
- };
1343
-
1344
- const directImports = await resolveModule(jsContent, componentPath);
1345
- // The component's direct imports drive its class-factory bindings.
1346
- for (const { depName, bindings } of directImports) {
1347
- if (dependencyContents[depName]) {
1348
- dependencyContents[depName].bindings = bindings;
1349
- }
1350
- }
1351
-
1352
- return dependencyContents;
1353
- }
1354
-
1355
- /**
1356
- * Cleans JavaScript code by removing imports/exports and ensuring class is available globally
1357
- */
1358
- cleanJavaScript(code, componentName, sourceContext = componentName) {
1359
- // Remove export default
1360
- code = code.replace(/export\s+default\s+/g, '');
1361
-
1362
- // Remove only unsupported imports (relative always removed, allowed absolute kept)
1363
- const stripped = this.stripImports(code, {
1364
- sourceContext,
1365
- collectHoistedImports: true
1366
- });
1367
- const hoistedImports = stripped.hoistedImports || [];
1368
- code = stripped.code;
1369
-
1370
- // Guard customElements.define to avoid duplicate registrations
1371
- code = code.replace(
1372
- /customElements\.define\(([^)]+)\);?/g,
1373
- (match, args) => {
1374
- const firstArg = args.split(',')[0]?.trim() || '';
1375
- if (!/^['"][^'"]+['"]$/.test(firstArg)) {
1376
- return match;
1377
- }
1378
- return `if (!customElements.get(${firstArg})) { customElements.define(${args}); }`;
1379
- }
1380
- );
1381
-
1382
- // Make sure the class is available globally for bundle evaluation
1383
- // Preserve original customElements.define if it exists
1384
- if (code.includes('customElements.define')) {
1385
- // Add global assignment before guarded or direct customElements.define
1386
- const globalAssignment = `window.${componentName} = ${componentName};\n`;
1387
- const guardedDefineRegex = /if\s*\(\s*!\s*customElements\.get\([^)]*\)\s*\)\s*\{\s*customElements\.define\([^;]+\);?\s*\}\s*$/;
1388
- const directDefineRegex = /customElements\.define\([^;]+\);?\s*$/;
1389
- if (guardedDefineRegex.test(code)) {
1390
- code = code.replace(guardedDefineRegex, `${globalAssignment}$&`);
1391
- } else {
1392
- code = code.replace(directDefineRegex, `${globalAssignment}$&`);
1393
- }
1394
- } else {
1395
- // If no customElements.define found, just assign to global
1396
- code += `\nwindow.${componentName} = ${componentName};`;
1397
- }
1398
-
1399
- // Add return statement for bundle evaluation compatibility
1400
- code += `\nreturn ${componentName};`;
1401
-
1402
- return {
1403
- code,
1404
- hoistedImports
1405
- };
1406
- }
1407
-
1408
- /**
1409
- * Formats the bundle file
1410
- */
1411
- formatBundleFile(componentsData, metadata) {
1412
- const integrityPayload = {
1413
- metadata: {
1414
- ...metadata,
1415
- generated: 'static'
1416
- },
1417
- components: Object.fromEntries(
1418
- Object.entries(componentsData).map(([name, data]) => [
1419
- name,
1420
- {
1421
- name: data.name,
1422
- category: data.category,
1423
- categoryType: data.categoryType,
1424
- componentDependencies: data.componentDependencies
1425
- }
1426
- ])
1427
- )
1428
- };
1429
- const integrity = `sha256:${crypto
1430
- .createHash('sha256')
1431
- .update(JSON.stringify(integrityPayload))
1432
- .digest('hex')}`;
1433
-
1434
- const dependencyModules = this.collectDependencyModules(componentsData);
1435
- const frameworkComponentKeys = Object.keys(componentsData || {});
1436
- const frameworkClassIdentifiers = frameworkComponentKeys.map((key) => this.toSafeIdentifier(key));
1437
- const frameworkReservedIdentifiers = new Set([
1438
- 'SLICE_BUNDLE',
1439
- 'SLICE_BUNDLE_COMPONENTS',
1440
- 'SLICE_BUNDLE_DEPENDENCIES',
1441
- 'SLICE_FRAMEWORK_CLASSES',
1442
- ...frameworkClassIdentifiers,
1443
- ...this.getDependencyExportVariableNames(dependencyModules)
1444
- ]);
1445
- const rawHoistedImports = Object.values(componentsData || {})
1446
- .flatMap((component) => component?.hoistedImports || [])
1447
- .map((statement) => String(statement).trim())
1448
- .filter(Boolean);
1449
- this.validateHoistedImportCollisions(rawHoistedImports, frameworkReservedIdentifiers);
1450
- const hoistedImportBlock = Array.from(new Set(rawHoistedImports)).join('\n');
1451
-
1452
- const dependencyBlock = this.buildDependencyModuleBlock(componentsData);
1453
- const componentBlock = this.buildComponentBundleBlock(componentsData);
1454
-
1455
- return `${hoistedImportBlock}${hoistedImportBlock ? '\n\n' : ''}/**
1456
- * Slice.js Bundle
1457
- * Type: ${metadata.type}
1458
- * Generated: ${metadata.generated}
1459
- * Strategy: ${metadata.strategy}
1460
- * Components: ${metadata.componentCount}
1461
- * Total Size: ${(metadata.totalSize / 1024).toFixed(1)} KB
1462
- */
1463
-
1464
- ${dependencyBlock}
1465
- ${componentBlock}
1466
-
1467
- export const SLICE_BUNDLE = {
1468
- metadata: ${JSON.stringify({ ...metadata, integrity }, null, 2)},
1469
- components: SLICE_BUNDLE_COMPONENTS
1470
- };
1471
-
1472
- // Auto-registration of components
1473
- if (window.slice && window.slice.controller) {
1474
- slice.controller.registerBundle(SLICE_BUNDLE);
1475
- }
1476
- `;
1477
- }
1478
-
1479
- buildDependencyModuleBlock(componentsData) {
1480
- const dependencyModules = this.collectDependencyModules(componentsData);
1481
- const lines = [
1482
- 'const SLICE_BUNDLE_DEPENDENCIES = {};',
1483
- ...this.getDefaultExportResolverLines()
1484
- ];
1485
- if (dependencyModules.length === 0) {
1486
- return `${lines.join('\n')}`;
1487
- }
1488
-
1489
- dependencyModules.forEach((module, index) => {
1490
- const exportVar = `__sliceDepExports${index}`;
1491
- // Each dependency lives in its own IIFE scope (see
1492
- // buildV2DependencyModuleBlockFromModules) so private helpers cannot
1493
- // collide across modules.
1494
- const content = this.transformDependencyContent(module.content, '__sliceExports', module.name);
1495
- const body = content.trim();
1496
- lines.push(`// Dependency: ${module.name}`);
1497
- lines.push(`const ${exportVar} = (() => {`);
1498
- lines.push('const __sliceExports = {};');
1499
- if (body) lines.push(body);
1500
- lines.push('return __sliceExports;');
1501
- lines.push('})();');
1502
- lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
1503
- });
1504
-
1505
- return `${lines.join('\n')}`;
1506
- }
1507
-
1508
- collectDependencyModules(componentsData) {
1509
- const modules = new Map();
1510
- Object.values(componentsData).forEach((component) => {
1511
- Object.entries(component.externalDependencies || {}).forEach(([name, entry]) => {
1512
- if (modules.has(name)) return;
1513
- const content = typeof entry === 'string' ? entry : entry.content;
1514
- modules.set(name, { name, content });
1515
- });
1516
- });
1517
- return Array.from(modules.values());
1518
- }
1519
-
1520
- collectDependencyModulesFromComponents(components = []) {
1521
- const modules = new Map();
1522
- for (const component of components || []) {
1523
- const externalDependencies = component.externalDependencies || {};
1524
- for (const [moduleName, entry] of Object.entries(externalDependencies)) {
1525
- if (modules.has(moduleName)) continue;
1526
- const content = typeof entry === 'string' ? entry : entry?.content;
1527
- if (!content) continue;
1528
- const moduleImports = (entry && typeof entry === 'object' ? entry.moduleImports : null) || [];
1529
- modules.set(moduleName, { name: moduleName, content, moduleImports });
1530
- }
1531
- }
1532
- return Array.from(modules.values());
1533
- }
1534
-
1535
- getDependencyExportVariableNames(dependencyModules = []) {
1536
- return (dependencyModules || []).map((_, index) => `__sliceDepExports${index}`);
1537
- }
1538
-
1539
- transformDependencyContent(content, exportVar, moduleName) {
1540
- let ast;
1541
- try {
1542
- ast = parse(content, { sourceType: 'module', plugins: ['jsx'] });
1543
- } catch (error) {
1544
- // Unparseable content (e.g. TS syntax): fall back to the regex transform
1545
- // so we never lose a dependency entirely.
1546
- return this.transformDependencyContentRegexFallback(content, exportVar, moduleName);
1547
- }
1548
-
1549
- const fallbackKey = this.getDependencyDefaultFallbackKey(moduleName);
1550
- const statements = ast.program.body
1551
- .filter((node) => typeof node.start === 'number' && typeof node.end === 'number')
1552
- .sort((a, b) => a.start - b.start);
1553
-
1554
- let cursor = 0;
1555
- let output = '';
1556
- for (const node of statements) {
1557
- output += content.slice(cursor, node.start);
1558
- output += this.transformDependencyStatement(node, content, exportVar, moduleName, fallbackKey);
1559
- cursor = node.end;
1560
- }
1561
- output += content.slice(cursor);
1562
- return output;
1563
- }
1564
-
1565
- describeExportTarget(exportVar, name) {
1566
- return /^[A-Za-z_$][\w$]*$/.test(name)
1567
- ? `${exportVar}.${name}`
1568
- : `${exportVar}[${JSON.stringify(name)}]`;
1569
- }
1570
-
1571
- collectPatternIdentifiers(node, acc = []) {
1572
- if (!node) return acc;
1573
- switch (node.type) {
1574
- case 'Identifier':
1575
- acc.push(node.name);
1576
- break;
1577
- case 'ObjectPattern':
1578
- for (const prop of node.properties) {
1579
- if (prop.type === 'RestElement') {
1580
- this.collectPatternIdentifiers(prop.argument, acc);
1581
- } else {
1582
- this.collectPatternIdentifiers(prop.value, acc);
1583
- }
1584
- }
1585
- break;
1586
- case 'ArrayPattern':
1587
- for (const element of node.elements) {
1588
- if (element) this.collectPatternIdentifiers(element, acc);
1589
- }
1590
- break;
1591
- case 'AssignmentPattern':
1592
- this.collectPatternIdentifiers(node.left, acc);
1593
- break;
1594
- case 'RestElement':
1595
- this.collectPatternIdentifiers(node.argument, acc);
1596
- break;
1597
- default:
1598
- break;
1599
- }
1600
- return acc;
1601
- }
1602
-
1603
- transformExportedDeclaration(decl, content, exportVar) {
1604
- const sourceOf = (n) => content.slice(n.start, n.end);
1605
-
1606
- if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
1607
- const name = decl.id?.name;
1608
- if (!name) return sourceOf(decl);
1609
- // Keep the declaration so other code in the module can still reference the
1610
- // name (intra-module references), then mirror it onto the exports object.
1611
- // Each dependency is IIFE-scoped, so this local binding can't collide.
1612
- return `${sourceOf(decl)}\n${this.describeExportTarget(exportVar, name)} = ${name};`;
1613
- }
1614
-
1615
- if (decl.type === 'VariableDeclaration') {
1616
- // Keep the declaration verbatim — preserving intra-module references and
1617
- // initializer evaluation — then export every bound name.
1618
- const names = [];
1619
- for (const declarator of decl.declarations) {
1620
- names.push(...this.collectPatternIdentifiers(declarator.id, []));
1621
- }
1622
- const assigns = names
1623
- .map((n) => `${this.describeExportTarget(exportVar, n)} = ${n};`)
1624
- .join('\n');
1625
- return `${sourceOf(decl)}\n${assigns}`;
1626
- }
1627
-
1628
- return sourceOf(decl);
1629
- }
1630
-
1631
- transformDependencyStatement(node, content, exportVar, moduleName, fallbackKey) {
1632
- const sourceOf = (n) => content.slice(n.start, n.end);
1633
-
1634
- if (node.type === 'ImportDeclaration') {
1635
- // Transitive imports of a bundled dependency cannot be resolved at
1636
- // runtime; strip them so they never leak into the emitted bundle.
1637
- console.warn(this.buildImportWarningMessage(
1638
- `Warning: Stripping unsupported import inside bundled dependency: ${node.source?.value}`,
1639
- moduleName
1640
- ));
1641
- return '';
1642
- }
1643
-
1644
- if (node.type === 'ExportAllDeclaration') {
1645
- console.warn(this.buildImportWarningMessage(
1646
- `Warning: Dropping unsupported 'export *' inside bundled dependency: ${node.source?.value}`,
1647
- moduleName
1648
- ));
1649
- return '';
1650
- }
1651
-
1652
- if (node.type === 'ExportDefaultDeclaration') {
1653
- const declSource = sourceOf(node.declaration);
1654
- const lines = [`${exportVar}.default = (${declSource});`];
1655
- if (fallbackKey && fallbackKey !== 'default') {
1656
- // Preserve the historical `<basename>Data` key so existing default
1657
- // bindings (which pass it as the preferred key) keep resolving.
1658
- lines.push(`${this.describeExportTarget(exportVar, fallbackKey)} = ${exportVar}.default;`);
1659
- }
1660
- return lines.join('\n');
1661
- }
1662
-
1663
- if (node.type === 'ExportNamedDeclaration') {
1664
- if (node.source) {
1665
- console.warn(this.buildImportWarningMessage(
1666
- `Warning: Dropping unsupported re-export inside bundled dependency: ${node.source.value}`,
1667
- moduleName
1668
- ));
1669
- return '';
1670
- }
1671
-
1672
- if (node.declaration) {
1673
- return this.transformExportedDeclaration(node.declaration, content, exportVar);
1674
- }
1675
-
1676
- // `export { local as exported, ... }` — key the exports object by the
1677
- // PUBLIC (exported) name, mapped to the local binding's value.
1678
- return node.specifiers
1679
- .map((spec) => {
1680
- const localName = spec.local.name;
1681
- const exportedName = spec.exported.name ?? spec.exported.value;
1682
- return `${this.describeExportTarget(exportVar, exportedName)} = ${localName};`;
1683
- })
1684
- .join('\n');
1685
- }
1686
-
1687
- // Any other top-level statement is kept verbatim.
1688
- return sourceOf(node);
1689
- }
1690
-
1691
- transformDependencyContentRegexFallback(content, exportVar, moduleName) {
1692
- const dataName = this.getDependencyDefaultFallbackKey(moduleName);
1693
- const exportPrefix = dataName ? `${exportVar}.${dataName} = ` : `${exportVar}.default = `;
1694
-
1695
- return content
1696
- .replace(/export\s+const\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
1697
- .replace(/export\s+let\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
1698
- .replace(/export\s+var\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
1699
- .replace(/export\s+function\s+(\w+)/g, `${exportVar}.$1 = function`)
1700
- .replace(/export\s+default\s+/g, exportPrefix)
1701
- .replace(/export\s*{\s*([^}]+)\s*}/g, (match, exportsStr) => {
1702
- return exportsStr
1703
- .split(',')
1704
- .map((exp) => {
1705
- const cleanExp = exp.trim();
1706
- const varName = cleanExp.split(' as ')[0].trim();
1707
- return `${exportVar}.${varName} = ${varName};`;
1708
- })
1709
- .join('\n');
1710
- })
1711
- .replace(/^\s*export\s+/gm, '');
1712
- }
1713
-
1714
- getDependencyDefaultFallbackKey(moduleName) {
1715
- const baseName = moduleName?.split('/').pop()?.replace(/\.[^.]+$/, '');
1716
- return baseName ? `${baseName}Data` : null;
1717
- }
1718
-
1719
- buildComponentBundleBlock(componentsData) {
1720
- const componentEntries = [];
1721
- const componentDefs = [];
1722
- const frameworkEntries = [];
1723
-
1724
- Object.entries(componentsData).forEach(([name, data]) => {
1725
- const classVar = this.toSafeIdentifier(name);
1726
- const bindings = this.buildDependencyBindings(data.externalDependencies || {});
1727
-
1728
- componentDefs.push(`const ${classVar} = (() => {\n${bindings}\n${data.js}\nreturn ${name};\n})();`);
1729
-
1730
- if (data.isFramework) {
1731
- frameworkEntries.push(`${JSON.stringify(data.name)}: ${classVar}`);
1732
- }
1733
-
1734
- componentEntries.push(
1735
- `${JSON.stringify(name)}: {\n` +
1736
- ` name: ${JSON.stringify(data.name)},\n` +
1737
- ` category: ${JSON.stringify(data.category)},\n` +
1738
- ` categoryType: ${JSON.stringify(data.categoryType)},\n` +
1739
- ` componentDependencies: ${JSON.stringify(data.componentDependencies)},\n` +
1740
- ` html: ${JSON.stringify(data.html)},\n` +
1741
- ` css: ${JSON.stringify(data.css)},\n` +
1742
- ` size: ${JSON.stringify(data.size)},\n` +
1743
- ` class: ${classVar}\n` +
1744
- `}`
1745
- );
1746
- });
1747
-
1748
- const frameworkBlock = frameworkEntries.length > 0
1749
- ? `const SLICE_FRAMEWORK_CLASSES = {\n${frameworkEntries.join(',\n')}\n};\nwindow.SLICE_FRAMEWORK_CLASSES = SLICE_FRAMEWORK_CLASSES;`
1750
- : '';
1751
-
1752
- return `${componentDefs.join('\n\n')}\n\nconst SLICE_BUNDLE_COMPONENTS = {\n${componentEntries.join(',\n')}\n};\n${frameworkBlock}`;
1753
- }
1754
-
1755
- buildDependencyBindings(externalDependencies, options = {}) {
1756
- const lines = [];
1757
- Object.entries(externalDependencies).forEach(([name, entry]) => {
1758
- const bindings = typeof entry === 'string' ? [] : entry.bindings || [];
1759
- const depVar = options.preferShared
1760
- ? `__sliceResolveBundleDependency(${JSON.stringify(name)})`
1761
- : `SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(name)}]`;
1762
-
1763
- bindings.forEach((binding) => {
1764
- if (!binding?.localName) return;
1765
- if (binding.type === 'default') {
1766
- const preferredKey = this.getDependencyDefaultFallbackKey(name);
1767
- lines.push(`const ${binding.localName} = __sliceResolveDefaultExport(${depVar}, ${JSON.stringify(name)}, ${JSON.stringify(preferredKey)});`);
1768
- }
1769
- if (binding.type === 'named') {
1770
- lines.push(`const ${binding.localName} = ${depVar}.${binding.importedName};`);
1771
- }
1772
- if (binding.type === 'namespace') {
1773
- lines.push(`const ${binding.localName} = ${depVar};`);
1774
- }
1775
- });
1776
- });
1777
-
1778
- return lines.join('\n');
1779
- }
1780
-
1781
- getBundleDependencyResolverLines() {
1782
- return [
1783
- "const __sliceSharedDeps = typeof window !== 'undefined' ? (window.__SLICE_SHARED_DEPS__ || {}) : {};",
1784
- 'const __sliceResolveBundleDependency = (depName) => Object.prototype.hasOwnProperty.call(__sliceSharedDeps, depName) ? __sliceSharedDeps[depName] : SLICE_BUNDLE_DEPENDENCIES[depName];'
1785
- ];
1786
- }
1787
-
1788
- getDefaultExportResolverLines() {
1789
- return [
1790
- 'const __sliceDefaultExportWarningDeps = new Set();',
1791
- "const __sliceDefaultExportPreferredKeys = ['module', 'exports', 'purify'];",
1792
- 'const __sliceDeterministicKeyCompare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);',
1793
- 'function __sliceResolveDefaultExport(dep, depName, preferredKey) {',
1794
- ' if (dep?.default !== undefined) return dep.default;',
1795
- " if (dep === null || (typeof dep !== 'object' && typeof dep !== 'function')) return dep;",
1796
- " if (preferredKey && preferredKey !== 'default' && preferredKey !== '__esModule' && Object.prototype.hasOwnProperty.call(dep, preferredKey)) return dep[preferredKey];",
1797
- " const keys = Object.keys(dep).filter((key) => key !== 'default' && key !== '__esModule');",
1798
- ' if (keys.length === 1) return dep[keys[0]];',
1799
- ' if (keys.length > 1) {',
1800
- ' const preferredMatches = __sliceDefaultExportPreferredKeys.filter((key) => keys.includes(key));',
1801
- ' if (preferredMatches.length === 1) return dep[preferredMatches[0]];',
1802
- ' const sortedKeys = [...keys].sort(__sliceDeterministicKeyCompare);',
1803
- ' const fallbackKey = sortedKeys[0];',
1804
- ' const warningDepName = depName || "<unknown dependency>";',
1805
- ' if (!__sliceDefaultExportWarningDeps.has(warningDepName)) {',
1806
- ' __sliceDefaultExportWarningDeps.add(warningDepName);',
1807
- ' console.warn(`[Slice.js bundler] Ambiguous default export resolution for "${warningDepName}". Falling back to "${fallbackKey}". Keys: ${sortedKeys.join(\', \')}`);',
1808
- ' }',
1809
- ' return dep[fallbackKey];',
1810
- ' }',
1811
- ' return dep;',
1812
- '}'
1813
- ];
1814
- }
1815
-
1816
- toSafeIdentifier(name) {
1817
- // Injective encoding: every character outside [A-Za-z0-9] (including '_')
1818
- // is escaped to `_<hex>_`. This guarantees that two distinct component
1819
- // names can never collapse to the same identifier (e.g. "my-btn" and
1820
- // "my_btn" used to both yield "SliceComponent_my_btn", emitting duplicate
1821
- // `const` declarations and producing invalid bundle JS). The leading
1822
- // `SliceComponent_` prefix keeps the result a valid identifier even when
1823
- // the name starts with a digit.
1824
- const encoded = String(name).replace(
1825
- /[^a-zA-Z0-9]/g,
1826
- (char) => `_${char.charCodeAt(0).toString(16)}_`
1827
- );
1828
- return `SliceComponent_${encoded}`;
1829
- }
1830
-
1831
- /**
1832
- * Generates the bundle configuration
1833
- */
1834
- generateBundleConfig(frameworkBundle = null) {
1835
- const metrics = this.analysisData.metrics || {};
1836
- const config = {
1837
- version: '2.0.0',
1838
- format: this.format,
1839
- loadingPolicy: this.loadingPolicy,
1840
- strategy: this.config.strategy,
1841
- minified: this.options.minify,
1842
- obfuscated: this.options.obfuscate,
1843
- production: true,
1844
- generated: new Date().toISOString(),
1845
-
1846
- stats: {
1847
- totalComponents: metrics.totalComponents || 0,
1848
- totalRoutes: metrics.totalRoutes || 0,
1849
- sharedComponents: this.bundles.critical.components.length,
1850
- sharedPercentage: metrics.sharedPercentage || 0,
1851
- totalSize: metrics.totalSize || 0,
1852
- criticalSize: this.bundles.critical.size
1853
- },
1854
-
1855
- bundles: {
1856
- framework: {
1857
- file: 'slice-bundle.framework.js',
1858
- size: 0,
1859
- hash: null,
1860
- integrity: null,
1861
- components: []
1862
- },
1863
- // Only advertise the vendor-shared bundle when it was actually emitted
1864
- // (i.e. there were shared dependencies). Otherwise the config would
1865
- // reference a file that does not exist on disk -> 404 for any runtime
1866
- // that resolves it.
1867
- vendorShared: this.vendorShared.bundle
1868
- ? {
1869
- bundleKey: 'vendor-shared',
1870
- type: 'vendor-shared',
1871
- file: this.vendorShared.file,
1872
- size: this.vendorShared.bundle?.size || 0,
1873
- hash: this.vendorShared.bundle?.hash || null,
1874
- integrity: this.vendorShared.bundle?.integrity || null,
1875
- dependencies: Array.from(this.vendorShared.sharedDependencySet).sort((a, b) => a.localeCompare(b)),
1876
- dependencyCount: this.vendorShared.sharedDependencySet.size,
1877
- routes: Array.from(this.vendorShared.bundleKeysUsingSharedDependencies).sort((a, b) => a.localeCompare(b))
1878
- }
1879
- : null,
1880
- critical: {
1881
- file: this.bundles.critical.file,
1882
- size: this.bundles.critical.size,
1883
- hash: this.bundles.critical.hash || null,
1884
- integrity: this.bundles.critical.integrity || null,
1885
- components: this.bundles.critical.components.map(c => c.name)
1886
- },
1887
- routes: {}
1888
- },
1889
- routeBundles: {},
1890
- routeDependencyGraph: {}
1891
- };
1892
-
1893
- for (const [key, bundle] of Object.entries(this.bundles.routes)) {
1894
- const routeIdentifier = Array.isArray(bundle.path || bundle.paths)
1895
- ? key
1896
- : (bundle.path || bundle.paths || key);
1897
- const usesVendorShared = this.vendorShared.bundleKeysUsingSharedDependencies.has(key)
1898
- || (bundle.dependencies || []).includes('vendor-shared');
1899
- const dependencies = this.mergeBundleDependencies(
1900
- bundle.dependencies || [],
1901
- usesVendorShared ? ['vendor-shared'] : []
1902
- );
1903
-
1904
- config.bundles.routes[key] = {
1905
- path: bundle.path || bundle.paths || key, // Support both single path and array of paths, fallback to key
1906
- file: `slice-bundle.${this.routeToFileName(routeIdentifier)}.js`,
1907
- size: bundle.size,
1908
- hash: bundle.hash || null,
1909
- integrity: bundle.integrity || null,
1910
- components: bundle.components.map(c => c.name),
1911
- dependencies
1912
- };
1913
-
1914
- const paths = Array.isArray(config.bundles.routes[key].path)
1915
- ? config.bundles.routes[key].path
1916
- : [config.bundles.routes[key].path];
1917
-
1918
- for (const routePath of paths) {
1919
- if (!config.routeBundles[routePath]) {
1920
- config.routeBundles[routePath] = ['critical'];
1921
- }
1922
- for (const dependency of dependencies.filter((dep) => dep !== 'critical')) {
1923
- if (!config.routeBundles[routePath].includes(dependency)) {
1924
- config.routeBundles[routePath].push(dependency);
1925
- }
1926
- }
1927
- if (!config.routeBundles[routePath].includes(key)) {
1928
- config.routeBundles[routePath].push(key);
1929
- }
1930
-
1931
- const graphEntry = config.routeDependencyGraph[routePath] || {
1932
- bundles: [],
1933
- edges: []
1934
- };
1935
- if (!graphEntry.bundles.includes(key)) {
1936
- graphEntry.bundles.push(key);
1937
- graphEntry.bundles.sort((a, b) => a.localeCompare(b));
1938
- }
1939
-
1940
- const edgeKeys = new Set(graphEntry.edges.map((edge) => `${edge.from}->${edge.to}`));
1941
- const orderedEdgeSources = ['critical', ...dependencies.filter((dependency) => dependency !== 'critical')];
1942
- for (const source of orderedEdgeSources) {
1943
- const edgeKey = `${source}->${key}`;
1944
- if (!edgeKeys.has(edgeKey)) {
1945
- graphEntry.edges.push({ from: source, to: key });
1946
- edgeKeys.add(edgeKey);
1947
- }
1948
- }
1949
-
1950
- config.routeDependencyGraph[routePath] = graphEntry;
1951
- }
1952
- }
1953
-
1954
- if (frameworkBundle) {
1955
- config.bundles.framework = {
1956
- file: frameworkBundle.file,
1957
- size: frameworkBundle.size,
1958
- hash: frameworkBundle.hash,
1959
- integrity: frameworkBundle.integrity,
1960
- components: frameworkBundle.components || []
1961
- };
1962
- }
1963
-
1964
- return config;
1965
- }
1966
-
1967
- collectFrameworkComponents() {
1968
- return this.analysisData.components.filter((comp) => comp.isFramework);
1969
- }
1970
-
1971
- async createFrameworkBundle(components) {
1972
- const fileName = 'slice-bundle.framework.js';
1973
- const filePath = path.join(this.bundlesPath, fileName);
1974
- return this.generateFrameworkBundleFile(components, fileName, filePath);
1975
- }
1976
-
1977
- async generateFrameworkBundleFile(components, fileName, filePath) {
1978
- const componentsData = {};
1979
- const componentsMap = await this.loadComponentsMap();
1980
- const metadata = {
1981
- version: '2.0.0',
1982
- type: 'framework',
1983
- route: null,
1984
- bundleKey: 'framework',
1985
- file: fileName,
1986
- generated: new Date().toISOString(),
1987
- totalSize: components.reduce((sum, c) => sum + c.size, 0),
1988
- componentCount: components.length,
1989
- strategy: this.config.strategy,
1990
- minified: this.options.minify,
1991
- obfuscated: this.options.obfuscate
1992
- };
1993
-
1994
- components.forEach((comp) => {
1995
- const componentKey = `Framework/Structural/${comp.name}`;
1996
- const fileBaseName = comp.fileName || comp.name;
1997
- const jsPath = path.join(comp.path, `${fileBaseName}.js`);
1998
- const jsContent = fs.readFileSync(jsPath, 'utf-8');
1999
- const dependencyContents = this.buildDependencyContentsSync(jsContent, comp.path);
2000
- const cleanedJavaScript = this.cleanJavaScript(jsContent, comp.name, jsPath);
2001
-
2002
- componentsData[componentKey] = {
2003
- name: comp.name,
2004
- category: comp.category,
2005
- categoryType: comp.categoryType,
2006
- isFramework: true,
2007
- js: cleanedJavaScript.code,
2008
- hoistedImports: cleanedJavaScript.hoistedImports,
2009
- externalDependencies: dependencyContents,
2010
- componentDependencies: Array.from(comp.dependencies),
2011
- html: fs.existsSync(path.join(comp.path, `${fileBaseName}.html`))
2012
- ? fs.readFileSync(path.join(comp.path, `${fileBaseName}.html`), 'utf-8')
2013
- : null,
2014
- css: fs.existsSync(path.join(comp.path, `${fileBaseName}.css`))
2015
- ? fs.readFileSync(path.join(comp.path, `${fileBaseName}.css`), 'utf-8')
2016
- : null,
2017
- size: comp.size
2018
- };
2019
- });
2020
-
2021
- const prelude = `const components = ${JSON.stringify(componentsMap)};`;
2022
- const bundleContent = `${prelude}\n${this.formatBundleFile(componentsData, metadata)}`;
2023
- const finalContent = await this.applyBundleTransforms(bundleContent, fileName);
2024
- await fs.ensureDir(path.dirname(filePath));
2025
- await fs.writeFile(filePath, finalContent, 'utf-8');
2026
-
2027
- const hash = crypto.createHash('sha256').update(finalContent).digest('hex');
2028
- const integrity = `sha256:${hash}`;
2029
-
2030
- return {
2031
- name: 'framework',
2032
- file: fileName,
2033
- size: Buffer.byteLength(bundleContent, 'utf-8'),
2034
- hash,
2035
- integrity,
2036
- componentCount: components.length,
2037
- components: components.map((comp) => `Framework/Structural/${comp.name}`)
2038
- };
2039
- }
2040
-
2041
- buildDependencyContentsSync(jsContent, componentPath) {
2042
- const dependencies = this.analyzeDependencies(jsContent, componentPath);
2043
- const dependencyContents = {};
2044
-
2045
- for (const dep of dependencies) {
2046
- const depPath = dep.path;
2047
- try {
2048
- const depContent = fs.readFileSync(depPath, 'utf-8');
2049
- const depName = path
2050
- .relative(this.srcPath, depPath)
2051
- .replace(/\\/g, '/');
2052
- dependencyContents[depName] = {
2053
- content: depContent,
2054
- bindings: dep.bindings || []
2055
- };
2056
- } catch (error) {
2057
- console.warn(`Warning: Could not read dependency ${depPath}:`, error.message);
2058
- }
2059
- }
2060
-
2061
- return dependencyContents;
2062
- }
2063
-
2064
- getConfiguredPublicFolders() {
2065
- const publicFolders = Array.isArray(this.sliceConfig?.publicFolders)
2066
- ? this.sliceConfig.publicFolders
2067
- : [];
2068
-
2069
- return publicFolders
2070
- .map((folder) => this.normalizePublicFolder(folder))
2071
- .filter(Boolean);
2072
- }
2073
-
2074
- normalizePublicFolder(folder) {
2075
- if (typeof folder !== 'string') return null;
2076
- let normalized = folder.trim();
2077
- if (!normalized) return null;
2078
-
2079
- if (!normalized.startsWith('/')) {
2080
- normalized = `/${normalized}`;
2081
- }
2082
-
2083
- normalized = normalized.replace(/\\+/g, '/').replace(/\/+/g, '/');
2084
- if (normalized.length > 1 && normalized.endsWith('/')) {
2085
- normalized = normalized.slice(0, -1);
2086
- }
2087
-
2088
- return normalized;
2089
- }
2090
-
2091
- normalizeImportPath(importPath) {
2092
- if (typeof importPath !== 'string') return '';
2093
- const cleanPath = importPath.split(/[?#]/)[0];
2094
- return cleanPath.replace(/\\+/g, '/').replace(/\/+/g, '/');
2095
- }
2096
-
2097
- isRelativeImport(importPath) {
2098
- return importPath.startsWith('./') || importPath.startsWith('../');
2099
- }
2100
-
2101
- isAbsoluteImport(importPath) {
2102
- return importPath.startsWith('/');
2103
- }
2104
-
2105
- isImportInPublicFolders(importPath, publicFolders) {
2106
- const normalizedImport = this.normalizeImportPath(importPath);
2107
- return publicFolders.some((folder) => normalizedImport === folder || normalizedImport.startsWith(`${folder}/`));
2108
- }
2109
-
2110
- classifyImport(importPath, publicFolders) {
2111
- if (typeof importPath !== 'string' || !importPath) {
2112
- return { keep: false, warning: 'Warning: Removing bare import: <unknown>' };
2113
- }
2114
-
2115
- if (this.isRelativeImport(importPath)) {
2116
- return { keep: false, warning: null };
2117
- }
2118
-
2119
- if (this.isAbsoluteImport(importPath)) {
2120
- if (this.isImportInPublicFolders(importPath, publicFolders)) {
2121
- return { keep: true, warning: null };
2122
- }
2123
-
2124
- return {
2125
- keep: false,
2126
- warning: `Warning: Removing absolute import outside publicFolders: ${importPath}`
2127
- };
2128
- }
2129
-
2130
- return {
2131
- keep: false,
2132
- warning: `Warning: Removing bare import: ${importPath}`
2133
- };
2134
- }
2135
-
2136
- buildImportWarningMessage(baseMessage, sourceContext) {
2137
- if (!sourceContext) return baseMessage;
2138
- return `${baseMessage} [${sourceContext}]`;
2139
- }
2140
-
2141
- extractLocalBindingsFromImportStatement(statement) {
2142
- const source = String(statement || '').trim();
2143
- if (!source.startsWith('import ')) return [];
2144
- if (/^import\s+['"][^'"]+['"]\s*;?$/.test(source)) return [];
2145
-
2146
- const bindings = [];
2147
-
2148
- const defaultMatch = source.match(/^import\s+([A-Za-z_$][\w$]*)\s*(,|\s+from\s+)/);
2149
- if (defaultMatch && defaultMatch[1] !== '*') {
2150
- bindings.push(defaultMatch[1]);
2151
- }
2152
-
2153
- const namespaceMatch = source.match(/,?\s*\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"]/);
2154
- if (namespaceMatch) {
2155
- bindings.push(namespaceMatch[1]);
2156
- }
2157
-
2158
- const namedMatch = source.match(/\{([\s\S]*?)\}\s*from\s*['"]/);
2159
- if (namedMatch) {
2160
- const namedSection = namedMatch[1];
2161
- for (const part of namedSection.split(',')) {
2162
- const cleanPart = part.trim();
2163
- if (!cleanPart) continue;
2164
- const aliasParts = cleanPart.split(/\s+as\s+/i).map((v) => v.trim()).filter(Boolean);
2165
- const localName = aliasParts.length > 1 ? aliasParts[1] : aliasParts[0];
2166
- if (/^[A-Za-z_$][\w$]*$/.test(localName)) {
2167
- bindings.push(localName);
2168
- }
2169
- }
2170
- }
2171
-
2172
- return Array.from(new Set(bindings));
2173
- }
2174
-
2175
- validateHoistedImportCollisions(importStatements, reservedIdentifiers = new Set()) {
2176
- const reserved = reservedIdentifiers instanceof Set
2177
- ? reservedIdentifiers
2178
- : new Set(reservedIdentifiers || []);
2179
- const bindingToStatement = new Map();
2180
-
2181
- for (const statement of importStatements || []) {
2182
- const normalizedStatement = String(statement || '').trim();
2183
- if (!normalizedStatement) continue;
2184
- const localBindings = this.extractLocalBindingsFromImportStatement(normalizedStatement);
2185
-
2186
- for (const localBinding of localBindings) {
2187
- if (reserved.has(localBinding)) {
2188
- throw new Error(`Hoisted import reserved identifier collision: ${localBinding}`);
2189
- }
2190
- const previousStatement = bindingToStatement.get(localBinding);
2191
- if (previousStatement && previousStatement !== normalizedStatement) {
2192
- throw new Error(`Hoisted import binding collision: ${localBinding}`);
2193
- }
2194
- bindingToStatement.set(localBinding, normalizedStatement);
2195
- }
2196
- }
2197
- }
2198
-
2199
- parseImportsFromCode(code) {
2200
- const ast = parse(code, {
2201
- sourceType: 'module',
2202
- plugins: ['jsx']
2203
- });
2204
-
2205
- const importNodes = [];
2206
- traverse.default(ast, {
2207
- ImportDeclaration(pathNode) {
2208
- importNodes.push(pathNode.node);
2209
- }
2210
- });
2211
-
2212
- return importNodes
2213
- .filter((node) => typeof node.start === 'number' && typeof node.end === 'number')
2214
- .sort((a, b) => a.start - b.start);
2215
- }
2216
-
2217
- parseImportsWithFallbackScanner(code) {
2218
- const entries = [];
2219
- const importRegex = /\bimport\b/g;
2220
- let match = null;
2221
-
2222
- while ((match = importRegex.exec(code)) !== null) {
2223
- const start = match.index;
2224
- const nextChar = code[start + 'import'.length];
2225
- if (nextChar === '(') {
2226
- continue;
2227
- }
2228
-
2229
- let index = start + 'import'.length;
2230
- let quote = null;
2231
- let escaped = false;
2232
-
2233
- while (index < code.length) {
2234
- const char = code[index];
2235
-
2236
- if (quote) {
2237
- if (escaped) {
2238
- escaped = false;
2239
- } else if (char === '\\') {
2240
- escaped = true;
2241
- } else if (char === quote) {
2242
- quote = null;
2243
- }
2244
- index += 1;
2245
- continue;
2246
- }
2247
-
2248
- if (char === '\'' || char === '"' || char === '`') {
2249
- quote = char;
2250
- index += 1;
2251
- continue;
2252
- }
2253
-
2254
- if (char === ';') {
2255
- index += 1;
2256
- break;
2257
- }
2258
-
2259
- index += 1;
2260
- }
2261
-
2262
- const end = index;
2263
- const statement = code.slice(start, end);
2264
- const fromMatch = statement.match(/\bfrom\s+['"]([^'"]+)['"]/);
2265
- const sideEffectMatch = statement.match(/\bimport\s+['"]([^'"]+)['"]/);
2266
- const importPath = fromMatch?.[1] || sideEffectMatch?.[1] || null;
2267
-
2268
- if (!importPath) {
2269
- continue;
2270
- }
2271
-
2272
- entries.push({ start, end, statement, importPath });
2273
- importRegex.lastIndex = end;
2274
- }
2275
-
2276
- return entries;
2277
- }
2278
-
2279
- stripImportsWithFallbackRegex(code, publicFolders, sourceContext, collectHoistedImports) {
2280
- const hoistedImports = [];
2281
- const importEntries = this.parseImportsWithFallbackScanner(code);
2282
- if (importEntries.length === 0) {
2283
- return { code, hoistedImports };
2284
- }
2285
-
2286
- let cleanedCode = '';
2287
- let cursor = 0;
2288
- for (const entry of importEntries) {
2289
- const { start, end, statement, importPath } = entry;
2290
- const classification = this.classifyImport(importPath, publicFolders);
2291
- cleanedCode += code.slice(cursor, start);
2292
- if (classification.keep) {
2293
- if (collectHoistedImports) {
2294
- hoistedImports.push(statement.trim());
2295
- } else {
2296
- cleanedCode += statement;
2297
- }
2298
- } else if (classification.warning) {
2299
- console.warn(this.buildImportWarningMessage(classification.warning, sourceContext));
2300
- }
2301
- cursor = end;
2302
- }
2303
-
2304
- cleanedCode += code.slice(cursor);
2305
-
2306
- return { code: cleanedCode, hoistedImports };
2307
- }
2308
-
2309
- stripImports(code, options = {}) {
2310
- const { sourceContext = null, collectHoistedImports = false } = options;
2311
- const publicFolders = this.getConfiguredPublicFolders();
2312
- const hoistedImports = [];
2313
-
2314
- try {
2315
- const importNodes = this.parseImportsFromCode(code);
2316
-
2317
- if (importNodes.length === 0) {
2318
- return collectHoistedImports ? { code, hoistedImports } : code;
2319
- }
2320
-
2321
- let cleaned = '';
2322
- let cursor = 0;
2323
-
2324
- for (const node of importNodes) {
2325
- const importPath = node.source?.value;
2326
- const classification = this.classifyImport(importPath, publicFolders);
2327
- const statement = code.slice(node.start, node.end);
2328
-
2329
- cleaned += code.slice(cursor, node.start);
2330
- if (classification.keep) {
2331
- if (collectHoistedImports) {
2332
- hoistedImports.push(statement.trim());
2333
- } else {
2334
- cleaned += statement;
2335
- }
2336
- } else if (classification.warning) {
2337
- console.warn(this.buildImportWarningMessage(classification.warning, sourceContext));
2338
- }
2339
-
2340
- cursor = node.end;
2341
- }
2342
-
2343
- cleaned += code.slice(cursor);
2344
- return collectHoistedImports
2345
- ? { code: cleaned, hoistedImports }
2346
- : cleaned;
2347
- } catch (error) {
2348
- const fallback = this.stripImportsWithFallbackRegex(code, publicFolders, sourceContext, collectHoistedImports);
2349
- return collectHoistedImports ? fallback : fallback.code;
2350
- }
2351
- }
2352
-
2353
- async loadComponentsMap() {
2354
- const componentsConfigPath = path.join(this.componentsPath, 'components.js');
2355
- if (!await fs.pathExists(componentsConfigPath)) {
2356
- return {};
2357
- }
2358
-
2359
- const content = await fs.readFile(componentsConfigPath, 'utf-8');
2360
- return this.parseComponentsConfig(content);
2361
- }
2362
-
2363
- parseComponentsConfig(content) {
2364
- try {
2365
- const ast = parse(content, {
2366
- sourceType: 'module',
2367
- plugins: ['jsx']
2368
- });
2369
-
2370
- let componentsNode = null;
2371
-
2372
- traverse.default(ast, {
2373
- VariableDeclarator(path) {
2374
- if (path.node.id?.type === 'Identifier' && path.node.id.name === 'components') {
2375
- componentsNode = path.node.init;
2376
- path.stop();
2377
- }
2378
- }
2379
- });
2380
-
2381
- if (!componentsNode || componentsNode.type !== 'ObjectExpression') {
2382
- throw new Error('components object not found');
2383
- }
2384
-
2385
- const config = {};
2386
- for (const prop of componentsNode.properties) {
2387
- if (prop.type !== 'ObjectProperty') continue;
2388
-
2389
- const key = this.extractStringValue(prop.key);
2390
- const value = this.extractStringValue(prop.value);
2391
-
2392
- if (!key || !value) {
2393
- throw new Error('Invalid components entry');
2394
- }
2395
-
2396
- config[key] = value;
2397
- }
2398
-
2399
- return config;
2400
- } catch (error) {
2401
- console.warn(`Could not parse components.js: ${error.message}`);
2402
- return {};
2403
- }
2404
- }
2405
-
2406
- extractStringValue(node) {
2407
- if (!node) return null;
2408
-
2409
- if (node.type === 'StringLiteral') {
2410
- return node.value;
2411
- }
2412
-
2413
- if (node.type === 'Identifier') {
2414
- return node.name;
2415
- }
2416
-
2417
- if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
2418
- return node.quasis.map((q) => q.value.cooked).join('');
2419
- }
2420
-
2421
- return null;
2422
- }
2423
-
2424
- /**
2425
- * Converts a route to filename
2426
- */
2427
- routeToFileName(routePath) {
2428
- if (routePath === '/') return 'home';
2429
- return routePath
2430
- .replace(/^\//, '')
2431
- .replace(/\//g, '-')
2432
- .replace(/[^a-zA-Z0-9-]/g, '')
2433
- .toLowerCase();
2434
- }
2435
-
2436
- /**
2437
- * Saves the configuration to file
2438
- */
2439
- async saveBundleConfig(config) {
2440
- // Ensure bundles directory exists
2441
- await fs.ensureDir(this.bundlesPath);
2442
-
2443
- // Save JSON config
2444
- const configPath = path.join(this.bundlesPath, 'bundle.config.json');
2445
- await fs.writeJson(configPath, config, { spaces: 2 });
2446
-
2447
- // Generate JavaScript module for direct import
2448
- const jsConfigPath = path.join(this.bundlesPath, 'bundle.config.js');
2449
- const jsConfig = this.generateBundleConfigJS(config);
2450
- await fs.writeFile(jsConfigPath, jsConfig, 'utf-8');
2451
-
2452
- console.log(`✓ Configuration saved to ${configPath}`);
2453
- console.log(`✓ JavaScript config generated: ${jsConfigPath}`);
2454
- }
2455
-
2456
- /**
2457
- * Creates a default bundle config file if none exists
2458
- */
2459
- async createDefaultBundleConfig() {
2460
- const defaultConfigPath = path.join(this.srcPath, 'bundles', 'bundle.config.js');
2461
-
2462
- // Only create if it doesn't exist
2463
- if (await fs.pathExists(defaultConfigPath)) {
2464
- return;
2465
- }
2466
-
2467
- await fs.ensureDir(path.dirname(defaultConfigPath));
2468
-
2469
- const defaultConfig = `/**
2470
- * Slice.js Bundle Configuration
2471
- * Default empty configuration - no bundles available
2472
- * Run 'slice build' to generate optimized bundles
2473
- */
2474
-
2475
- // No bundles available - using individual component loading
2476
- export const SLICE_BUNDLE_CONFIG = null;
2477
-
2478
- // No auto-initialization needed for default config
2479
- `;
2480
-
2481
- await fs.writeFile(defaultConfigPath, defaultConfig, 'utf-8');
2482
- console.log(`✓ Default bundle config created: ${defaultConfigPath}`);
2483
- }
2484
-
2485
- /**
2486
- * Generates JavaScript module for direct import
2487
- */
2488
- generateBundleConfigJS(config) {
2489
- return `/**
2490
- * Slice.js Bundle Configuration
2491
- * Generated: ${new Date().toISOString()}
2492
- * Strategy: ${config.strategy}
2493
- */
2494
-
2495
- // Direct bundle configuration (no fetch required)
2496
- export const SLICE_BUNDLE_CONFIG = ${JSON.stringify(config, null, 2)};
2497
-
2498
- // Auto-initialization if slice is available
2499
- if (typeof window !== 'undefined' && window.slice && window.slice.controller) {
2500
- window.slice.controller.bundleConfig = SLICE_BUNDLE_CONFIG;
2501
-
2502
- // Load critical bundle automatically
2503
- if (SLICE_BUNDLE_CONFIG.bundles.critical && !window.slice.controller.criticalBundleLoaded) {
2504
- (async () => {
2505
- const bundlePath = "/bundles/" + SLICE_BUNDLE_CONFIG.bundles.critical.file;
2506
- const integrity = SLICE_BUNDLE_CONFIG.bundles.critical.integrity;
2507
-
2508
- if (typeof window.slice.controller.verifyBundleIntegrity === 'function') {
2509
- const ok = await window.slice.controller.verifyBundleIntegrity(bundlePath, integrity);
2510
- if (!ok) {
2511
- console.warn('Failed to load critical bundle: integrity check failed');
2512
- return;
2513
- }
2514
- }
2515
-
2516
- import('./slice-bundle.critical.js').catch(err =>
2517
- console.warn('Failed to load critical bundle:', err)
2518
- );
2519
- window.slice.controller.criticalBundleLoaded = true;
2520
- })();
2521
- }
2522
- }
2523
- `;
2524
- }
2525
- }
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: { regex: /^_/ }
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
+ // Emit in topological order so a module is registered before any module
1253
+ // that depends on it (its transitive imports resolve at IIFE-eval time).
1254
+ const filteredModules = this.sortDependencyModulesTopologically(
1255
+ modules.filter((module) => !omittedDependencies.has(module.name))
1256
+ );
1257
+
1258
+ const lines = [
1259
+ 'const SLICE_BUNDLE_DEPENDENCIES = {};',
1260
+ ...this.getDefaultExportResolverLines()
1261
+ ];
1262
+ if (options.includeSharedResolver) {
1263
+ lines.push(...this.getBundleDependencyResolverLines());
1264
+ }
1265
+ filteredModules.forEach((module, index) => {
1266
+ const exportVar = `__sliceDepExports${index}`;
1267
+ // Evaluate each dependency inside its own IIFE so its private,
1268
+ // non-exported top-level bindings stay local and cannot collide with
1269
+ // another dependency's (or the bundle's) identifiers. Only the exports
1270
+ // object escapes the closure.
1271
+ const transformedContent = this.transformDependencyContent(module.content, '__sliceExports', module.name);
1272
+ // Bind this module's own (transitive) imports inside its IIFE — they were
1273
+ // registered by earlier modules in the topological order.
1274
+ const importBindings = this.buildDependencyBindings(
1275
+ Object.fromEntries(
1276
+ (module.moduleImports || [])
1277
+ .filter((mi) => mi.bindings && mi.bindings.length)
1278
+ .map((mi) => [mi.depName, { bindings: mi.bindings }])
1279
+ ),
1280
+ { preferShared: !!options.includeSharedResolver }
1281
+ );
1282
+ const body = transformedContent.trim();
1283
+ lines.push(`const ${exportVar} = (() => {`);
1284
+ lines.push('const __sliceExports = {};');
1285
+ if (importBindings) lines.push(importBindings);
1286
+ if (body) lines.push(body);
1287
+ lines.push('return __sliceExports;');
1288
+ lines.push('})();');
1289
+ lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
1290
+ });
1291
+
1292
+ return lines.join('\n');
1293
+ }
1294
+
1295
+ sortDependencyModulesTopologically(modules = []) {
1296
+ const byName = new Map(modules.map((module) => [module.name, module]));
1297
+ const visited = new Set();
1298
+ const ordered = [];
1299
+ const visit = (module, stack) => {
1300
+ if (visited.has(module.name) || stack.has(module.name)) return;
1301
+ stack.add(module.name);
1302
+ for (const imp of module.moduleImports || []) {
1303
+ const dependency = byName.get(imp.depName);
1304
+ if (dependency) visit(dependency, stack);
1305
+ }
1306
+ stack.delete(module.name);
1307
+ visited.add(module.name);
1308
+ ordered.push(module);
1309
+ };
1310
+ for (const module of modules) visit(module, new Set());
1311
+ return ordered;
1312
+ }
1313
+
1314
+ async buildDependencyContents(jsContent, componentPath) {
1315
+ const dependencyContents = {};
1316
+ const visited = new Set();
1317
+
1318
+ // Recursively resolve the relative-import graph rooted at `content`, so a
1319
+ // dependency module's OWN (transitive) imports are inlined too. Returns the
1320
+ // consumer's direct imports as [{ depName, bindings }].
1321
+ const resolveModule = async (content, basePath) => {
1322
+ const consumerImports = [];
1323
+
1324
+ for (const dep of this.analyzeDependencies(content, basePath)) {
1325
+ const depName = path.relative(this.srcPath, dep.path).replace(/\\/g, '/');
1326
+ consumerImports.push({ depName, bindings: dep.bindings || [] });
1327
+
1328
+ if (visited.has(depName)) continue;
1329
+ visited.add(depName);
1330
+
1331
+ try {
1332
+ const depContent = await fs.readFile(dep.path, 'utf-8');
1333
+ // Resolve this module's own transitive imports first.
1334
+ const moduleImports = await resolveModule(depContent, path.dirname(dep.path));
1335
+ dependencyContents[depName] = { content: depContent, bindings: [], moduleImports };
1336
+ } catch (error) {
1337
+ console.warn(`Warning: Could not read dependency ${dep.path}:`, error.message);
1338
+ }
1339
+ }
1340
+
1341
+ return consumerImports;
1342
+ };
1343
+
1344
+ const directImports = await resolveModule(jsContent, componentPath);
1345
+ // The component's direct imports drive its class-factory bindings.
1346
+ for (const { depName, bindings } of directImports) {
1347
+ if (dependencyContents[depName]) {
1348
+ dependencyContents[depName].bindings = bindings;
1349
+ }
1350
+ }
1351
+
1352
+ return dependencyContents;
1353
+ }
1354
+
1355
+ /**
1356
+ * Cleans JavaScript code by removing imports/exports and ensuring class is available globally
1357
+ */
1358
+ cleanJavaScript(code, componentName, sourceContext = componentName) {
1359
+ // Remove export default
1360
+ code = code.replace(/export\s+default\s+/g, '');
1361
+
1362
+ // Remove only unsupported imports (relative always removed, allowed absolute kept)
1363
+ const stripped = this.stripImports(code, {
1364
+ sourceContext,
1365
+ collectHoistedImports: true
1366
+ });
1367
+ const hoistedImports = stripped.hoistedImports || [];
1368
+ code = stripped.code;
1369
+
1370
+ // Guard customElements.define to avoid duplicate registrations
1371
+ code = code.replace(
1372
+ /customElements\.define\(([^)]+)\);?/g,
1373
+ (match, args) => {
1374
+ const firstArg = args.split(',')[0]?.trim() || '';
1375
+ if (!/^['"][^'"]+['"]$/.test(firstArg)) {
1376
+ return match;
1377
+ }
1378
+ return `if (!customElements.get(${firstArg})) { customElements.define(${args}); }`;
1379
+ }
1380
+ );
1381
+
1382
+ // Make sure the class is available globally for bundle evaluation
1383
+ // Preserve original customElements.define if it exists
1384
+ if (code.includes('customElements.define')) {
1385
+ // Add global assignment before guarded or direct customElements.define
1386
+ const globalAssignment = `window.${componentName} = ${componentName};\n`;
1387
+ const guardedDefineRegex = /if\s*\(\s*!\s*customElements\.get\([^)]*\)\s*\)\s*\{\s*customElements\.define\([^;]+\);?\s*\}\s*$/;
1388
+ const directDefineRegex = /customElements\.define\([^;]+\);?\s*$/;
1389
+ if (guardedDefineRegex.test(code)) {
1390
+ code = code.replace(guardedDefineRegex, `${globalAssignment}$&`);
1391
+ } else {
1392
+ code = code.replace(directDefineRegex, `${globalAssignment}$&`);
1393
+ }
1394
+ } else {
1395
+ // If no customElements.define found, just assign to global
1396
+ code += `\nwindow.${componentName} = ${componentName};`;
1397
+ }
1398
+
1399
+ // Add return statement for bundle evaluation compatibility
1400
+ code += `\nreturn ${componentName};`;
1401
+
1402
+ return {
1403
+ code,
1404
+ hoistedImports
1405
+ };
1406
+ }
1407
+
1408
+ /**
1409
+ * Formats the bundle file
1410
+ */
1411
+ formatBundleFile(componentsData, metadata) {
1412
+ const integrityPayload = {
1413
+ metadata: {
1414
+ ...metadata,
1415
+ generated: 'static'
1416
+ },
1417
+ components: Object.fromEntries(
1418
+ Object.entries(componentsData).map(([name, data]) => [
1419
+ name,
1420
+ {
1421
+ name: data.name,
1422
+ category: data.category,
1423
+ categoryType: data.categoryType,
1424
+ componentDependencies: data.componentDependencies
1425
+ }
1426
+ ])
1427
+ )
1428
+ };
1429
+ const integrity = `sha256:${crypto
1430
+ .createHash('sha256')
1431
+ .update(JSON.stringify(integrityPayload))
1432
+ .digest('hex')}`;
1433
+
1434
+ const dependencyModules = this.collectDependencyModules(componentsData);
1435
+ const frameworkComponentKeys = Object.keys(componentsData || {});
1436
+ const frameworkClassIdentifiers = frameworkComponentKeys.map((key) => this.toSafeIdentifier(key));
1437
+ const frameworkReservedIdentifiers = new Set([
1438
+ 'SLICE_BUNDLE',
1439
+ 'SLICE_BUNDLE_COMPONENTS',
1440
+ 'SLICE_BUNDLE_DEPENDENCIES',
1441
+ 'SLICE_FRAMEWORK_CLASSES',
1442
+ ...frameworkClassIdentifiers,
1443
+ ...this.getDependencyExportVariableNames(dependencyModules)
1444
+ ]);
1445
+ const rawHoistedImports = Object.values(componentsData || {})
1446
+ .flatMap((component) => component?.hoistedImports || [])
1447
+ .map((statement) => String(statement).trim())
1448
+ .filter(Boolean);
1449
+ this.validateHoistedImportCollisions(rawHoistedImports, frameworkReservedIdentifiers);
1450
+ const hoistedImportBlock = Array.from(new Set(rawHoistedImports)).join('\n');
1451
+
1452
+ const dependencyBlock = this.buildDependencyModuleBlock(componentsData);
1453
+ const componentBlock = this.buildComponentBundleBlock(componentsData);
1454
+
1455
+ return `${hoistedImportBlock}${hoistedImportBlock ? '\n\n' : ''}/**
1456
+ * Slice.js Bundle
1457
+ * Type: ${metadata.type}
1458
+ * Generated: ${metadata.generated}
1459
+ * Strategy: ${metadata.strategy}
1460
+ * Components: ${metadata.componentCount}
1461
+ * Total Size: ${(metadata.totalSize / 1024).toFixed(1)} KB
1462
+ */
1463
+
1464
+ ${dependencyBlock}
1465
+ ${componentBlock}
1466
+
1467
+ export const SLICE_BUNDLE = {
1468
+ metadata: ${JSON.stringify({ ...metadata, integrity }, null, 2)},
1469
+ components: SLICE_BUNDLE_COMPONENTS
1470
+ };
1471
+
1472
+ // Auto-registration of components
1473
+ if (window.slice && window.slice.controller) {
1474
+ slice.controller.registerBundle(SLICE_BUNDLE);
1475
+ }
1476
+ `;
1477
+ }
1478
+
1479
+ buildDependencyModuleBlock(componentsData) {
1480
+ const dependencyModules = this.collectDependencyModules(componentsData);
1481
+ const lines = [
1482
+ 'const SLICE_BUNDLE_DEPENDENCIES = {};',
1483
+ ...this.getDefaultExportResolverLines()
1484
+ ];
1485
+ if (dependencyModules.length === 0) {
1486
+ return `${lines.join('\n')}`;
1487
+ }
1488
+
1489
+ dependencyModules.forEach((module, index) => {
1490
+ const exportVar = `__sliceDepExports${index}`;
1491
+ // Each dependency lives in its own IIFE scope (see
1492
+ // buildV2DependencyModuleBlockFromModules) so private helpers cannot
1493
+ // collide across modules.
1494
+ const content = this.transformDependencyContent(module.content, '__sliceExports', module.name);
1495
+ const body = content.trim();
1496
+ lines.push(`// Dependency: ${module.name}`);
1497
+ lines.push(`const ${exportVar} = (() => {`);
1498
+ lines.push('const __sliceExports = {};');
1499
+ if (body) lines.push(body);
1500
+ lines.push('return __sliceExports;');
1501
+ lines.push('})();');
1502
+ lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
1503
+ });
1504
+
1505
+ return `${lines.join('\n')}`;
1506
+ }
1507
+
1508
+ collectDependencyModules(componentsData) {
1509
+ const modules = new Map();
1510
+ Object.values(componentsData).forEach((component) => {
1511
+ Object.entries(component.externalDependencies || {}).forEach(([name, entry]) => {
1512
+ if (modules.has(name)) return;
1513
+ const content = typeof entry === 'string' ? entry : entry.content;
1514
+ modules.set(name, { name, content });
1515
+ });
1516
+ });
1517
+ return Array.from(modules.values());
1518
+ }
1519
+
1520
+ collectDependencyModulesFromComponents(components = []) {
1521
+ const modules = new Map();
1522
+ for (const component of components || []) {
1523
+ const externalDependencies = component.externalDependencies || {};
1524
+ for (const [moduleName, entry] of Object.entries(externalDependencies)) {
1525
+ if (modules.has(moduleName)) continue;
1526
+ const content = typeof entry === 'string' ? entry : entry?.content;
1527
+ if (!content) continue;
1528
+ const moduleImports = (entry && typeof entry === 'object' ? entry.moduleImports : null) || [];
1529
+ modules.set(moduleName, { name: moduleName, content, moduleImports });
1530
+ }
1531
+ }
1532
+ return Array.from(modules.values());
1533
+ }
1534
+
1535
+ getDependencyExportVariableNames(dependencyModules = []) {
1536
+ return (dependencyModules || []).map((_, index) => `__sliceDepExports${index}`);
1537
+ }
1538
+
1539
+ transformDependencyContent(content, exportVar, moduleName) {
1540
+ let ast;
1541
+ try {
1542
+ ast = parse(content, { sourceType: 'module', plugins: ['jsx'] });
1543
+ } catch (error) {
1544
+ // Unparseable content (e.g. TS syntax): fall back to the regex transform
1545
+ // so we never lose a dependency entirely.
1546
+ return this.transformDependencyContentRegexFallback(content, exportVar, moduleName);
1547
+ }
1548
+
1549
+ const fallbackKey = this.getDependencyDefaultFallbackKey(moduleName);
1550
+ const statements = ast.program.body
1551
+ .filter((node) => typeof node.start === 'number' && typeof node.end === 'number')
1552
+ .sort((a, b) => a.start - b.start);
1553
+
1554
+ let cursor = 0;
1555
+ let output = '';
1556
+ for (const node of statements) {
1557
+ output += content.slice(cursor, node.start);
1558
+ output += this.transformDependencyStatement(node, content, exportVar, moduleName, fallbackKey);
1559
+ cursor = node.end;
1560
+ }
1561
+ output += content.slice(cursor);
1562
+ return output;
1563
+ }
1564
+
1565
+ describeExportTarget(exportVar, name) {
1566
+ return /^[A-Za-z_$][\w$]*$/.test(name)
1567
+ ? `${exportVar}.${name}`
1568
+ : `${exportVar}[${JSON.stringify(name)}]`;
1569
+ }
1570
+
1571
+ collectPatternIdentifiers(node, acc = []) {
1572
+ if (!node) return acc;
1573
+ switch (node.type) {
1574
+ case 'Identifier':
1575
+ acc.push(node.name);
1576
+ break;
1577
+ case 'ObjectPattern':
1578
+ for (const prop of node.properties) {
1579
+ if (prop.type === 'RestElement') {
1580
+ this.collectPatternIdentifiers(prop.argument, acc);
1581
+ } else {
1582
+ this.collectPatternIdentifiers(prop.value, acc);
1583
+ }
1584
+ }
1585
+ break;
1586
+ case 'ArrayPattern':
1587
+ for (const element of node.elements) {
1588
+ if (element) this.collectPatternIdentifiers(element, acc);
1589
+ }
1590
+ break;
1591
+ case 'AssignmentPattern':
1592
+ this.collectPatternIdentifiers(node.left, acc);
1593
+ break;
1594
+ case 'RestElement':
1595
+ this.collectPatternIdentifiers(node.argument, acc);
1596
+ break;
1597
+ default:
1598
+ break;
1599
+ }
1600
+ return acc;
1601
+ }
1602
+
1603
+ transformExportedDeclaration(decl, content, exportVar) {
1604
+ const sourceOf = (n) => content.slice(n.start, n.end);
1605
+
1606
+ if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
1607
+ const name = decl.id?.name;
1608
+ if (!name) return sourceOf(decl);
1609
+ // Keep the declaration so other code in the module can still reference the
1610
+ // name (intra-module references), then mirror it onto the exports object.
1611
+ // Each dependency is IIFE-scoped, so this local binding can't collide.
1612
+ return `${sourceOf(decl)}\n${this.describeExportTarget(exportVar, name)} = ${name};`;
1613
+ }
1614
+
1615
+ if (decl.type === 'VariableDeclaration') {
1616
+ // Keep the declaration verbatim — preserving intra-module references and
1617
+ // initializer evaluation — then export every bound name.
1618
+ const names = [];
1619
+ for (const declarator of decl.declarations) {
1620
+ names.push(...this.collectPatternIdentifiers(declarator.id, []));
1621
+ }
1622
+ const assigns = names
1623
+ .map((n) => `${this.describeExportTarget(exportVar, n)} = ${n};`)
1624
+ .join('\n');
1625
+ return `${sourceOf(decl)}\n${assigns}`;
1626
+ }
1627
+
1628
+ return sourceOf(decl);
1629
+ }
1630
+
1631
+ transformDependencyStatement(node, content, exportVar, moduleName, fallbackKey) {
1632
+ const sourceOf = (n) => content.slice(n.start, n.end);
1633
+
1634
+ if (node.type === 'ImportDeclaration') {
1635
+ // Transitive imports of a bundled dependency cannot be resolved at
1636
+ // runtime; strip them so they never leak into the emitted bundle.
1637
+ console.warn(this.buildImportWarningMessage(
1638
+ `Warning: Stripping unsupported import inside bundled dependency: ${node.source?.value}`,
1639
+ moduleName
1640
+ ));
1641
+ return '';
1642
+ }
1643
+
1644
+ if (node.type === 'ExportAllDeclaration') {
1645
+ console.warn(this.buildImportWarningMessage(
1646
+ `Warning: Dropping unsupported 'export *' inside bundled dependency: ${node.source?.value}`,
1647
+ moduleName
1648
+ ));
1649
+ return '';
1650
+ }
1651
+
1652
+ if (node.type === 'ExportDefaultDeclaration') {
1653
+ const declSource = sourceOf(node.declaration);
1654
+ const lines = [`${exportVar}.default = (${declSource});`];
1655
+ if (fallbackKey && fallbackKey !== 'default') {
1656
+ // Preserve the historical `<basename>Data` key so existing default
1657
+ // bindings (which pass it as the preferred key) keep resolving.
1658
+ lines.push(`${this.describeExportTarget(exportVar, fallbackKey)} = ${exportVar}.default;`);
1659
+ }
1660
+ return lines.join('\n');
1661
+ }
1662
+
1663
+ if (node.type === 'ExportNamedDeclaration') {
1664
+ if (node.source) {
1665
+ console.warn(this.buildImportWarningMessage(
1666
+ `Warning: Dropping unsupported re-export inside bundled dependency: ${node.source.value}`,
1667
+ moduleName
1668
+ ));
1669
+ return '';
1670
+ }
1671
+
1672
+ if (node.declaration) {
1673
+ return this.transformExportedDeclaration(node.declaration, content, exportVar);
1674
+ }
1675
+
1676
+ // `export { local as exported, ... }` — key the exports object by the
1677
+ // PUBLIC (exported) name, mapped to the local binding's value.
1678
+ return node.specifiers
1679
+ .map((spec) => {
1680
+ const localName = spec.local.name;
1681
+ const exportedName = spec.exported.name ?? spec.exported.value;
1682
+ return `${this.describeExportTarget(exportVar, exportedName)} = ${localName};`;
1683
+ })
1684
+ .join('\n');
1685
+ }
1686
+
1687
+ // Any other top-level statement is kept verbatim.
1688
+ return sourceOf(node);
1689
+ }
1690
+
1691
+ transformDependencyContentRegexFallback(content, exportVar, moduleName) {
1692
+ const dataName = this.getDependencyDefaultFallbackKey(moduleName);
1693
+ const exportPrefix = dataName ? `${exportVar}.${dataName} = ` : `${exportVar}.default = `;
1694
+
1695
+ return content
1696
+ .replace(/export\s+const\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
1697
+ .replace(/export\s+let\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
1698
+ .replace(/export\s+var\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
1699
+ .replace(/export\s+function\s+(\w+)/g, `${exportVar}.$1 = function`)
1700
+ .replace(/export\s+default\s+/g, exportPrefix)
1701
+ .replace(/export\s*{\s*([^}]+)\s*}/g, (match, exportsStr) => {
1702
+ return exportsStr
1703
+ .split(',')
1704
+ .map((exp) => {
1705
+ const cleanExp = exp.trim();
1706
+ const varName = cleanExp.split(' as ')[0].trim();
1707
+ return `${exportVar}.${varName} = ${varName};`;
1708
+ })
1709
+ .join('\n');
1710
+ })
1711
+ .replace(/^\s*export\s+/gm, '');
1712
+ }
1713
+
1714
+ getDependencyDefaultFallbackKey(moduleName) {
1715
+ const baseName = moduleName?.split('/').pop()?.replace(/\.[^.]+$/, '');
1716
+ return baseName ? `${baseName}Data` : null;
1717
+ }
1718
+
1719
+ buildComponentBundleBlock(componentsData) {
1720
+ const componentEntries = [];
1721
+ const componentDefs = [];
1722
+ const frameworkEntries = [];
1723
+
1724
+ Object.entries(componentsData).forEach(([name, data]) => {
1725
+ const classVar = this.toSafeIdentifier(name);
1726
+ const bindings = this.buildDependencyBindings(data.externalDependencies || {});
1727
+
1728
+ componentDefs.push(`const ${classVar} = (() => {\n${bindings}\n${data.js}\nreturn ${name};\n})();`);
1729
+
1730
+ if (data.isFramework) {
1731
+ frameworkEntries.push(`${JSON.stringify(data.name)}: ${classVar}`);
1732
+ }
1733
+
1734
+ componentEntries.push(
1735
+ `${JSON.stringify(name)}: {\n` +
1736
+ ` name: ${JSON.stringify(data.name)},\n` +
1737
+ ` category: ${JSON.stringify(data.category)},\n` +
1738
+ ` categoryType: ${JSON.stringify(data.categoryType)},\n` +
1739
+ ` componentDependencies: ${JSON.stringify(data.componentDependencies)},\n` +
1740
+ ` html: ${JSON.stringify(data.html)},\n` +
1741
+ ` css: ${JSON.stringify(data.css)},\n` +
1742
+ ` size: ${JSON.stringify(data.size)},\n` +
1743
+ ` class: ${classVar}\n` +
1744
+ `}`
1745
+ );
1746
+ });
1747
+
1748
+ const frameworkBlock = frameworkEntries.length > 0
1749
+ ? `const SLICE_FRAMEWORK_CLASSES = {\n${frameworkEntries.join(',\n')}\n};\nwindow.SLICE_FRAMEWORK_CLASSES = SLICE_FRAMEWORK_CLASSES;`
1750
+ : '';
1751
+
1752
+ return `${componentDefs.join('\n\n')}\n\nconst SLICE_BUNDLE_COMPONENTS = {\n${componentEntries.join(',\n')}\n};\n${frameworkBlock}`;
1753
+ }
1754
+
1755
+ buildDependencyBindings(externalDependencies, options = {}) {
1756
+ const lines = [];
1757
+ Object.entries(externalDependencies).forEach(([name, entry]) => {
1758
+ const bindings = typeof entry === 'string' ? [] : entry.bindings || [];
1759
+ const depVar = options.preferShared
1760
+ ? `__sliceResolveBundleDependency(${JSON.stringify(name)})`
1761
+ : `SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(name)}]`;
1762
+
1763
+ bindings.forEach((binding) => {
1764
+ if (!binding?.localName) return;
1765
+ if (binding.type === 'default') {
1766
+ const preferredKey = this.getDependencyDefaultFallbackKey(name);
1767
+ lines.push(`const ${binding.localName} = __sliceResolveDefaultExport(${depVar}, ${JSON.stringify(name)}, ${JSON.stringify(preferredKey)});`);
1768
+ }
1769
+ if (binding.type === 'named') {
1770
+ lines.push(`const ${binding.localName} = ${depVar}.${binding.importedName};`);
1771
+ }
1772
+ if (binding.type === 'namespace') {
1773
+ lines.push(`const ${binding.localName} = ${depVar};`);
1774
+ }
1775
+ });
1776
+ });
1777
+
1778
+ return lines.join('\n');
1779
+ }
1780
+
1781
+ getBundleDependencyResolverLines() {
1782
+ return [
1783
+ "const __sliceSharedDeps = typeof window !== 'undefined' ? (window.__SLICE_SHARED_DEPS__ || {}) : {};",
1784
+ 'const __sliceResolveBundleDependency = (depName) => Object.prototype.hasOwnProperty.call(__sliceSharedDeps, depName) ? __sliceSharedDeps[depName] : SLICE_BUNDLE_DEPENDENCIES[depName];'
1785
+ ];
1786
+ }
1787
+
1788
+ getDefaultExportResolverLines() {
1789
+ return [
1790
+ 'const __sliceDefaultExportWarningDeps = new Set();',
1791
+ "const __sliceDefaultExportPreferredKeys = ['module', 'exports', 'purify'];",
1792
+ 'const __sliceDeterministicKeyCompare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);',
1793
+ 'function __sliceResolveDefaultExport(dep, depName, preferredKey) {',
1794
+ ' if (dep?.default !== undefined) return dep.default;',
1795
+ " if (dep === null || (typeof dep !== 'object' && typeof dep !== 'function')) return dep;",
1796
+ " if (preferredKey && preferredKey !== 'default' && preferredKey !== '__esModule' && Object.prototype.hasOwnProperty.call(dep, preferredKey)) return dep[preferredKey];",
1797
+ " const keys = Object.keys(dep).filter((key) => key !== 'default' && key !== '__esModule');",
1798
+ ' if (keys.length === 1) return dep[keys[0]];',
1799
+ ' if (keys.length > 1) {',
1800
+ ' const preferredMatches = __sliceDefaultExportPreferredKeys.filter((key) => keys.includes(key));',
1801
+ ' if (preferredMatches.length === 1) return dep[preferredMatches[0]];',
1802
+ ' const sortedKeys = [...keys].sort(__sliceDeterministicKeyCompare);',
1803
+ ' const fallbackKey = sortedKeys[0];',
1804
+ ' const warningDepName = depName || "<unknown dependency>";',
1805
+ ' if (!__sliceDefaultExportWarningDeps.has(warningDepName)) {',
1806
+ ' __sliceDefaultExportWarningDeps.add(warningDepName);',
1807
+ ' console.warn(`[Slice.js bundler] Ambiguous default export resolution for "${warningDepName}". Falling back to "${fallbackKey}". Keys: ${sortedKeys.join(\', \')}`);',
1808
+ ' }',
1809
+ ' return dep[fallbackKey];',
1810
+ ' }',
1811
+ ' return dep;',
1812
+ '}'
1813
+ ];
1814
+ }
1815
+
1816
+ toSafeIdentifier(name) {
1817
+ // Injective encoding: every character outside [A-Za-z0-9] (including '_')
1818
+ // is escaped to `_<hex>_`. This guarantees that two distinct component
1819
+ // names can never collapse to the same identifier (e.g. "my-btn" and
1820
+ // "my_btn" used to both yield "SliceComponent_my_btn", emitting duplicate
1821
+ // `const` declarations and producing invalid bundle JS). The leading
1822
+ // `SliceComponent_` prefix keeps the result a valid identifier even when
1823
+ // the name starts with a digit.
1824
+ const encoded = String(name).replace(
1825
+ /[^a-zA-Z0-9]/g,
1826
+ (char) => `_${char.charCodeAt(0).toString(16)}_`
1827
+ );
1828
+ return `SliceComponent_${encoded}`;
1829
+ }
1830
+
1831
+ /**
1832
+ * Generates the bundle configuration
1833
+ */
1834
+ generateBundleConfig(frameworkBundle = null) {
1835
+ const metrics = this.analysisData.metrics || {};
1836
+ const config = {
1837
+ version: '2.0.0',
1838
+ format: this.format,
1839
+ loadingPolicy: this.loadingPolicy,
1840
+ strategy: this.config.strategy,
1841
+ minified: this.options.minify,
1842
+ obfuscated: this.options.obfuscate,
1843
+ production: true,
1844
+ generated: new Date().toISOString(),
1845
+
1846
+ stats: {
1847
+ totalComponents: metrics.totalComponents || 0,
1848
+ totalRoutes: metrics.totalRoutes || 0,
1849
+ sharedComponents: this.bundles.critical.components.length,
1850
+ sharedPercentage: metrics.sharedPercentage || 0,
1851
+ totalSize: metrics.totalSize || 0,
1852
+ criticalSize: this.bundles.critical.size
1853
+ },
1854
+
1855
+ bundles: {
1856
+ framework: {
1857
+ file: 'slice-bundle.framework.js',
1858
+ size: 0,
1859
+ hash: null,
1860
+ integrity: null,
1861
+ components: []
1862
+ },
1863
+ // Only advertise the vendor-shared bundle when it was actually emitted
1864
+ // (i.e. there were shared dependencies). Otherwise the config would
1865
+ // reference a file that does not exist on disk -> 404 for any runtime
1866
+ // that resolves it.
1867
+ vendorShared: this.vendorShared.bundle
1868
+ ? {
1869
+ bundleKey: 'vendor-shared',
1870
+ type: 'vendor-shared',
1871
+ file: this.vendorShared.file,
1872
+ size: this.vendorShared.bundle?.size || 0,
1873
+ hash: this.vendorShared.bundle?.hash || null,
1874
+ integrity: this.vendorShared.bundle?.integrity || null,
1875
+ dependencies: Array.from(this.vendorShared.sharedDependencySet).sort((a, b) => a.localeCompare(b)),
1876
+ dependencyCount: this.vendorShared.sharedDependencySet.size,
1877
+ routes: Array.from(this.vendorShared.bundleKeysUsingSharedDependencies).sort((a, b) => a.localeCompare(b))
1878
+ }
1879
+ : null,
1880
+ critical: {
1881
+ file: this.bundles.critical.file,
1882
+ size: this.bundles.critical.size,
1883
+ hash: this.bundles.critical.hash || null,
1884
+ integrity: this.bundles.critical.integrity || null,
1885
+ components: this.bundles.critical.components.map(c => c.name)
1886
+ },
1887
+ routes: {}
1888
+ },
1889
+ routeBundles: {},
1890
+ routeDependencyGraph: {}
1891
+ };
1892
+
1893
+ for (const [key, bundle] of Object.entries(this.bundles.routes)) {
1894
+ const routeIdentifier = Array.isArray(bundle.path || bundle.paths)
1895
+ ? key
1896
+ : (bundle.path || bundle.paths || key);
1897
+ const usesVendorShared = this.vendorShared.bundleKeysUsingSharedDependencies.has(key)
1898
+ || (bundle.dependencies || []).includes('vendor-shared');
1899
+ const dependencies = this.mergeBundleDependencies(
1900
+ bundle.dependencies || [],
1901
+ usesVendorShared ? ['vendor-shared'] : []
1902
+ );
1903
+
1904
+ config.bundles.routes[key] = {
1905
+ path: bundle.path || bundle.paths || key, // Support both single path and array of paths, fallback to key
1906
+ file: `slice-bundle.${this.routeToFileName(routeIdentifier)}.js`,
1907
+ size: bundle.size,
1908
+ hash: bundle.hash || null,
1909
+ integrity: bundle.integrity || null,
1910
+ components: bundle.components.map(c => c.name),
1911
+ dependencies
1912
+ };
1913
+
1914
+ const paths = Array.isArray(config.bundles.routes[key].path)
1915
+ ? config.bundles.routes[key].path
1916
+ : [config.bundles.routes[key].path];
1917
+
1918
+ for (const routePath of paths) {
1919
+ if (!config.routeBundles[routePath]) {
1920
+ config.routeBundles[routePath] = ['critical'];
1921
+ }
1922
+ for (const dependency of dependencies.filter((dep) => dep !== 'critical')) {
1923
+ if (!config.routeBundles[routePath].includes(dependency)) {
1924
+ config.routeBundles[routePath].push(dependency);
1925
+ }
1926
+ }
1927
+ if (!config.routeBundles[routePath].includes(key)) {
1928
+ config.routeBundles[routePath].push(key);
1929
+ }
1930
+
1931
+ const graphEntry = config.routeDependencyGraph[routePath] || {
1932
+ bundles: [],
1933
+ edges: []
1934
+ };
1935
+ if (!graphEntry.bundles.includes(key)) {
1936
+ graphEntry.bundles.push(key);
1937
+ graphEntry.bundles.sort((a, b) => a.localeCompare(b));
1938
+ }
1939
+
1940
+ const edgeKeys = new Set(graphEntry.edges.map((edge) => `${edge.from}->${edge.to}`));
1941
+ const orderedEdgeSources = ['critical', ...dependencies.filter((dependency) => dependency !== 'critical')];
1942
+ for (const source of orderedEdgeSources) {
1943
+ const edgeKey = `${source}->${key}`;
1944
+ if (!edgeKeys.has(edgeKey)) {
1945
+ graphEntry.edges.push({ from: source, to: key });
1946
+ edgeKeys.add(edgeKey);
1947
+ }
1948
+ }
1949
+
1950
+ config.routeDependencyGraph[routePath] = graphEntry;
1951
+ }
1952
+ }
1953
+
1954
+ if (frameworkBundle) {
1955
+ config.bundles.framework = {
1956
+ file: frameworkBundle.file,
1957
+ size: frameworkBundle.size,
1958
+ hash: frameworkBundle.hash,
1959
+ integrity: frameworkBundle.integrity,
1960
+ components: frameworkBundle.components || []
1961
+ };
1962
+ }
1963
+
1964
+ return config;
1965
+ }
1966
+
1967
+ collectFrameworkComponents() {
1968
+ return this.analysisData.components.filter((comp) => comp.isFramework);
1969
+ }
1970
+
1971
+ async createFrameworkBundle(components) {
1972
+ const fileName = 'slice-bundle.framework.js';
1973
+ const filePath = path.join(this.bundlesPath, fileName);
1974
+ return this.generateFrameworkBundleFile(components, fileName, filePath);
1975
+ }
1976
+
1977
+ async generateFrameworkBundleFile(components, fileName, filePath) {
1978
+ const componentsData = {};
1979
+ const componentsMap = await this.loadComponentsMap();
1980
+ const metadata = {
1981
+ version: '2.0.0',
1982
+ type: 'framework',
1983
+ route: null,
1984
+ bundleKey: 'framework',
1985
+ file: fileName,
1986
+ generated: new Date().toISOString(),
1987
+ totalSize: components.reduce((sum, c) => sum + c.size, 0),
1988
+ componentCount: components.length,
1989
+ strategy: this.config.strategy,
1990
+ minified: this.options.minify,
1991
+ obfuscated: this.options.obfuscate
1992
+ };
1993
+
1994
+ components.forEach((comp) => {
1995
+ const componentKey = `Framework/Structural/${comp.name}`;
1996
+ const fileBaseName = comp.fileName || comp.name;
1997
+ const jsPath = path.join(comp.path, `${fileBaseName}.js`);
1998
+ const jsContent = fs.readFileSync(jsPath, 'utf-8');
1999
+ const dependencyContents = this.buildDependencyContentsSync(jsContent, comp.path);
2000
+ const cleanedJavaScript = this.cleanJavaScript(jsContent, comp.name, jsPath);
2001
+
2002
+ componentsData[componentKey] = {
2003
+ name: comp.name,
2004
+ category: comp.category,
2005
+ categoryType: comp.categoryType,
2006
+ isFramework: true,
2007
+ js: cleanedJavaScript.code,
2008
+ hoistedImports: cleanedJavaScript.hoistedImports,
2009
+ externalDependencies: dependencyContents,
2010
+ componentDependencies: Array.from(comp.dependencies),
2011
+ html: fs.existsSync(path.join(comp.path, `${fileBaseName}.html`))
2012
+ ? fs.readFileSync(path.join(comp.path, `${fileBaseName}.html`), 'utf-8')
2013
+ : null,
2014
+ css: fs.existsSync(path.join(comp.path, `${fileBaseName}.css`))
2015
+ ? fs.readFileSync(path.join(comp.path, `${fileBaseName}.css`), 'utf-8')
2016
+ : null,
2017
+ size: comp.size
2018
+ };
2019
+ });
2020
+
2021
+ const prelude = `const components = ${JSON.stringify(componentsMap)};`;
2022
+ const bundleContent = `${prelude}\n${this.formatBundleFile(componentsData, metadata)}`;
2023
+ const finalContent = await this.applyBundleTransforms(bundleContent, fileName);
2024
+ await fs.ensureDir(path.dirname(filePath));
2025
+ await fs.writeFile(filePath, finalContent, 'utf-8');
2026
+
2027
+ const hash = crypto.createHash('sha256').update(finalContent).digest('hex');
2028
+ const integrity = `sha256:${hash}`;
2029
+
2030
+ return {
2031
+ name: 'framework',
2032
+ file: fileName,
2033
+ size: Buffer.byteLength(bundleContent, 'utf-8'),
2034
+ hash,
2035
+ integrity,
2036
+ componentCount: components.length,
2037
+ components: components.map((comp) => `Framework/Structural/${comp.name}`)
2038
+ };
2039
+ }
2040
+
2041
+ buildDependencyContentsSync(jsContent, componentPath) {
2042
+ const dependencies = this.analyzeDependencies(jsContent, componentPath);
2043
+ const dependencyContents = {};
2044
+
2045
+ for (const dep of dependencies) {
2046
+ const depPath = dep.path;
2047
+ try {
2048
+ const depContent = fs.readFileSync(depPath, 'utf-8');
2049
+ const depName = path
2050
+ .relative(this.srcPath, depPath)
2051
+ .replace(/\\/g, '/');
2052
+ dependencyContents[depName] = {
2053
+ content: depContent,
2054
+ bindings: dep.bindings || []
2055
+ };
2056
+ } catch (error) {
2057
+ console.warn(`Warning: Could not read dependency ${depPath}:`, error.message);
2058
+ }
2059
+ }
2060
+
2061
+ return dependencyContents;
2062
+ }
2063
+
2064
+ getConfiguredPublicFolders() {
2065
+ const publicFolders = Array.isArray(this.sliceConfig?.publicFolders)
2066
+ ? this.sliceConfig.publicFolders
2067
+ : [];
2068
+
2069
+ return publicFolders
2070
+ .map((folder) => this.normalizePublicFolder(folder))
2071
+ .filter(Boolean);
2072
+ }
2073
+
2074
+ normalizePublicFolder(folder) {
2075
+ if (typeof folder !== 'string') return null;
2076
+ let normalized = folder.trim();
2077
+ if (!normalized) return null;
2078
+
2079
+ if (!normalized.startsWith('/')) {
2080
+ normalized = `/${normalized}`;
2081
+ }
2082
+
2083
+ normalized = normalized.replace(/\\+/g, '/').replace(/\/+/g, '/');
2084
+ if (normalized.length > 1 && normalized.endsWith('/')) {
2085
+ normalized = normalized.slice(0, -1);
2086
+ }
2087
+
2088
+ return normalized;
2089
+ }
2090
+
2091
+ normalizeImportPath(importPath) {
2092
+ if (typeof importPath !== 'string') return '';
2093
+ const cleanPath = importPath.split(/[?#]/)[0];
2094
+ return cleanPath.replace(/\\+/g, '/').replace(/\/+/g, '/');
2095
+ }
2096
+
2097
+ isRelativeImport(importPath) {
2098
+ return importPath.startsWith('./') || importPath.startsWith('../');
2099
+ }
2100
+
2101
+ isAbsoluteImport(importPath) {
2102
+ return importPath.startsWith('/');
2103
+ }
2104
+
2105
+ isImportInPublicFolders(importPath, publicFolders) {
2106
+ const normalizedImport = this.normalizeImportPath(importPath);
2107
+ return publicFolders.some((folder) => normalizedImport === folder || normalizedImport.startsWith(`${folder}/`));
2108
+ }
2109
+
2110
+ classifyImport(importPath, publicFolders) {
2111
+ if (typeof importPath !== 'string' || !importPath) {
2112
+ return { keep: false, warning: 'Warning: Removing bare import: <unknown>' };
2113
+ }
2114
+
2115
+ if (this.isRelativeImport(importPath)) {
2116
+ return { keep: false, warning: null };
2117
+ }
2118
+
2119
+ if (this.isAbsoluteImport(importPath)) {
2120
+ if (this.isImportInPublicFolders(importPath, publicFolders)) {
2121
+ return { keep: true, warning: null };
2122
+ }
2123
+
2124
+ return {
2125
+ keep: false,
2126
+ warning: `Warning: Removing absolute import outside publicFolders: ${importPath}`
2127
+ };
2128
+ }
2129
+
2130
+ return {
2131
+ keep: false,
2132
+ warning: `Warning: Removing bare import: ${importPath}`
2133
+ };
2134
+ }
2135
+
2136
+ buildImportWarningMessage(baseMessage, sourceContext) {
2137
+ if (!sourceContext) return baseMessage;
2138
+ return `${baseMessage} [${sourceContext}]`;
2139
+ }
2140
+
2141
+ extractLocalBindingsFromImportStatement(statement) {
2142
+ const source = String(statement || '').trim();
2143
+ if (!source.startsWith('import ')) return [];
2144
+ if (/^import\s+['"][^'"]+['"]\s*;?$/.test(source)) return [];
2145
+
2146
+ const bindings = [];
2147
+
2148
+ const defaultMatch = source.match(/^import\s+([A-Za-z_$][\w$]*)\s*(,|\s+from\s+)/);
2149
+ if (defaultMatch && defaultMatch[1] !== '*') {
2150
+ bindings.push(defaultMatch[1]);
2151
+ }
2152
+
2153
+ const namespaceMatch = source.match(/,?\s*\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"]/);
2154
+ if (namespaceMatch) {
2155
+ bindings.push(namespaceMatch[1]);
2156
+ }
2157
+
2158
+ const namedMatch = source.match(/\{([\s\S]*?)\}\s*from\s*['"]/);
2159
+ if (namedMatch) {
2160
+ const namedSection = namedMatch[1];
2161
+ for (const part of namedSection.split(',')) {
2162
+ const cleanPart = part.trim();
2163
+ if (!cleanPart) continue;
2164
+ const aliasParts = cleanPart.split(/\s+as\s+/i).map((v) => v.trim()).filter(Boolean);
2165
+ const localName = aliasParts.length > 1 ? aliasParts[1] : aliasParts[0];
2166
+ if (/^[A-Za-z_$][\w$]*$/.test(localName)) {
2167
+ bindings.push(localName);
2168
+ }
2169
+ }
2170
+ }
2171
+
2172
+ return Array.from(new Set(bindings));
2173
+ }
2174
+
2175
+ validateHoistedImportCollisions(importStatements, reservedIdentifiers = new Set()) {
2176
+ const reserved = reservedIdentifiers instanceof Set
2177
+ ? reservedIdentifiers
2178
+ : new Set(reservedIdentifiers || []);
2179
+ const bindingToStatement = new Map();
2180
+
2181
+ for (const statement of importStatements || []) {
2182
+ const normalizedStatement = String(statement || '').trim();
2183
+ if (!normalizedStatement) continue;
2184
+ const localBindings = this.extractLocalBindingsFromImportStatement(normalizedStatement);
2185
+
2186
+ for (const localBinding of localBindings) {
2187
+ if (reserved.has(localBinding)) {
2188
+ throw new Error(`Hoisted import reserved identifier collision: ${localBinding}`);
2189
+ }
2190
+ const previousStatement = bindingToStatement.get(localBinding);
2191
+ if (previousStatement && previousStatement !== normalizedStatement) {
2192
+ throw new Error(`Hoisted import binding collision: ${localBinding}`);
2193
+ }
2194
+ bindingToStatement.set(localBinding, normalizedStatement);
2195
+ }
2196
+ }
2197
+ }
2198
+
2199
+ parseImportsFromCode(code) {
2200
+ const ast = parse(code, {
2201
+ sourceType: 'module',
2202
+ plugins: ['jsx']
2203
+ });
2204
+
2205
+ const importNodes = [];
2206
+ traverse.default(ast, {
2207
+ ImportDeclaration(pathNode) {
2208
+ importNodes.push(pathNode.node);
2209
+ }
2210
+ });
2211
+
2212
+ return importNodes
2213
+ .filter((node) => typeof node.start === 'number' && typeof node.end === 'number')
2214
+ .sort((a, b) => a.start - b.start);
2215
+ }
2216
+
2217
+ parseImportsWithFallbackScanner(code) {
2218
+ const entries = [];
2219
+ const importRegex = /\bimport\b/g;
2220
+ let match = null;
2221
+
2222
+ while ((match = importRegex.exec(code)) !== null) {
2223
+ const start = match.index;
2224
+ const nextChar = code[start + 'import'.length];
2225
+ if (nextChar === '(') {
2226
+ continue;
2227
+ }
2228
+
2229
+ let index = start + 'import'.length;
2230
+ let quote = null;
2231
+ let escaped = false;
2232
+
2233
+ while (index < code.length) {
2234
+ const char = code[index];
2235
+
2236
+ if (quote) {
2237
+ if (escaped) {
2238
+ escaped = false;
2239
+ } else if (char === '\\') {
2240
+ escaped = true;
2241
+ } else if (char === quote) {
2242
+ quote = null;
2243
+ }
2244
+ index += 1;
2245
+ continue;
2246
+ }
2247
+
2248
+ if (char === '\'' || char === '"' || char === '`') {
2249
+ quote = char;
2250
+ index += 1;
2251
+ continue;
2252
+ }
2253
+
2254
+ if (char === ';') {
2255
+ index += 1;
2256
+ break;
2257
+ }
2258
+
2259
+ index += 1;
2260
+ }
2261
+
2262
+ const end = index;
2263
+ const statement = code.slice(start, end);
2264
+ const fromMatch = statement.match(/\bfrom\s+['"]([^'"]+)['"]/);
2265
+ const sideEffectMatch = statement.match(/\bimport\s+['"]([^'"]+)['"]/);
2266
+ const importPath = fromMatch?.[1] || sideEffectMatch?.[1] || null;
2267
+
2268
+ if (!importPath) {
2269
+ continue;
2270
+ }
2271
+
2272
+ entries.push({ start, end, statement, importPath });
2273
+ importRegex.lastIndex = end;
2274
+ }
2275
+
2276
+ return entries;
2277
+ }
2278
+
2279
+ stripImportsWithFallbackRegex(code, publicFolders, sourceContext, collectHoistedImports) {
2280
+ const hoistedImports = [];
2281
+ const importEntries = this.parseImportsWithFallbackScanner(code);
2282
+ if (importEntries.length === 0) {
2283
+ return { code, hoistedImports };
2284
+ }
2285
+
2286
+ let cleanedCode = '';
2287
+ let cursor = 0;
2288
+ for (const entry of importEntries) {
2289
+ const { start, end, statement, importPath } = entry;
2290
+ const classification = this.classifyImport(importPath, publicFolders);
2291
+ cleanedCode += code.slice(cursor, start);
2292
+ if (classification.keep) {
2293
+ if (collectHoistedImports) {
2294
+ hoistedImports.push(statement.trim());
2295
+ } else {
2296
+ cleanedCode += statement;
2297
+ }
2298
+ } else if (classification.warning) {
2299
+ console.warn(this.buildImportWarningMessage(classification.warning, sourceContext));
2300
+ }
2301
+ cursor = end;
2302
+ }
2303
+
2304
+ cleanedCode += code.slice(cursor);
2305
+
2306
+ return { code: cleanedCode, hoistedImports };
2307
+ }
2308
+
2309
+ stripImports(code, options = {}) {
2310
+ const { sourceContext = null, collectHoistedImports = false } = options;
2311
+ const publicFolders = this.getConfiguredPublicFolders();
2312
+ const hoistedImports = [];
2313
+
2314
+ try {
2315
+ const importNodes = this.parseImportsFromCode(code);
2316
+
2317
+ if (importNodes.length === 0) {
2318
+ return collectHoistedImports ? { code, hoistedImports } : code;
2319
+ }
2320
+
2321
+ let cleaned = '';
2322
+ let cursor = 0;
2323
+
2324
+ for (const node of importNodes) {
2325
+ const importPath = node.source?.value;
2326
+ const classification = this.classifyImport(importPath, publicFolders);
2327
+ const statement = code.slice(node.start, node.end);
2328
+
2329
+ cleaned += code.slice(cursor, node.start);
2330
+ if (classification.keep) {
2331
+ if (collectHoistedImports) {
2332
+ hoistedImports.push(statement.trim());
2333
+ } else {
2334
+ cleaned += statement;
2335
+ }
2336
+ } else if (classification.warning) {
2337
+ console.warn(this.buildImportWarningMessage(classification.warning, sourceContext));
2338
+ }
2339
+
2340
+ cursor = node.end;
2341
+ }
2342
+
2343
+ cleaned += code.slice(cursor);
2344
+ return collectHoistedImports
2345
+ ? { code: cleaned, hoistedImports }
2346
+ : cleaned;
2347
+ } catch (error) {
2348
+ const fallback = this.stripImportsWithFallbackRegex(code, publicFolders, sourceContext, collectHoistedImports);
2349
+ return collectHoistedImports ? fallback : fallback.code;
2350
+ }
2351
+ }
2352
+
2353
+ async loadComponentsMap() {
2354
+ const componentsConfigPath = path.join(this.componentsPath, 'components.js');
2355
+ if (!await fs.pathExists(componentsConfigPath)) {
2356
+ return {};
2357
+ }
2358
+
2359
+ const content = await fs.readFile(componentsConfigPath, 'utf-8');
2360
+ return this.parseComponentsConfig(content);
2361
+ }
2362
+
2363
+ parseComponentsConfig(content) {
2364
+ try {
2365
+ const ast = parse(content, {
2366
+ sourceType: 'module',
2367
+ plugins: ['jsx']
2368
+ });
2369
+
2370
+ let componentsNode = null;
2371
+
2372
+ traverse.default(ast, {
2373
+ VariableDeclarator(path) {
2374
+ if (path.node.id?.type === 'Identifier' && path.node.id.name === 'components') {
2375
+ componentsNode = path.node.init;
2376
+ path.stop();
2377
+ }
2378
+ }
2379
+ });
2380
+
2381
+ if (!componentsNode || componentsNode.type !== 'ObjectExpression') {
2382
+ throw new Error('components object not found');
2383
+ }
2384
+
2385
+ const config = {};
2386
+ for (const prop of componentsNode.properties) {
2387
+ if (prop.type !== 'ObjectProperty') continue;
2388
+
2389
+ const key = this.extractStringValue(prop.key);
2390
+ const value = this.extractStringValue(prop.value);
2391
+
2392
+ if (!key || !value) {
2393
+ throw new Error('Invalid components entry');
2394
+ }
2395
+
2396
+ config[key] = value;
2397
+ }
2398
+
2399
+ return config;
2400
+ } catch (error) {
2401
+ console.warn(`Could not parse components.js: ${error.message}`);
2402
+ return {};
2403
+ }
2404
+ }
2405
+
2406
+ extractStringValue(node) {
2407
+ if (!node) return null;
2408
+
2409
+ if (node.type === 'StringLiteral') {
2410
+ return node.value;
2411
+ }
2412
+
2413
+ if (node.type === 'Identifier') {
2414
+ return node.name;
2415
+ }
2416
+
2417
+ if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
2418
+ return node.quasis.map((q) => q.value.cooked).join('');
2419
+ }
2420
+
2421
+ return null;
2422
+ }
2423
+
2424
+ /**
2425
+ * Converts a route to filename
2426
+ */
2427
+ routeToFileName(routePath) {
2428
+ if (routePath === '/') return 'home';
2429
+ return routePath
2430
+ .replace(/^\//, '')
2431
+ .replace(/\//g, '-')
2432
+ .replace(/[^a-zA-Z0-9-]/g, '')
2433
+ .toLowerCase();
2434
+ }
2435
+
2436
+ /**
2437
+ * Saves the configuration to file
2438
+ */
2439
+ async saveBundleConfig(config) {
2440
+ // Ensure bundles directory exists
2441
+ await fs.ensureDir(this.bundlesPath);
2442
+
2443
+ // Save JSON config
2444
+ const configPath = path.join(this.bundlesPath, 'bundle.config.json');
2445
+ await fs.writeJson(configPath, config, { spaces: 2 });
2446
+
2447
+ // Generate JavaScript module for direct import
2448
+ const jsConfigPath = path.join(this.bundlesPath, 'bundle.config.js');
2449
+ const jsConfig = this.generateBundleConfigJS(config);
2450
+ await fs.writeFile(jsConfigPath, jsConfig, 'utf-8');
2451
+
2452
+ console.log(`✓ Configuration saved to ${configPath}`);
2453
+ console.log(`✓ JavaScript config generated: ${jsConfigPath}`);
2454
+ }
2455
+
2456
+ /**
2457
+ * Creates a default bundle config file if none exists
2458
+ */
2459
+ async createDefaultBundleConfig() {
2460
+ const defaultConfigPath = path.join(this.srcPath, 'bundles', 'bundle.config.js');
2461
+
2462
+ // Only create if it doesn't exist
2463
+ if (await fs.pathExists(defaultConfigPath)) {
2464
+ return;
2465
+ }
2466
+
2467
+ await fs.ensureDir(path.dirname(defaultConfigPath));
2468
+
2469
+ const defaultConfig = `/**
2470
+ * Slice.js Bundle Configuration
2471
+ * Default empty configuration - no bundles available
2472
+ * Run 'slice build' to generate optimized bundles
2473
+ */
2474
+
2475
+ // No bundles available - using individual component loading
2476
+ export const SLICE_BUNDLE_CONFIG = null;
2477
+
2478
+ // No auto-initialization needed for default config
2479
+ `;
2480
+
2481
+ await fs.writeFile(defaultConfigPath, defaultConfig, 'utf-8');
2482
+ console.log(`✓ Default bundle config created: ${defaultConfigPath}`);
2483
+ }
2484
+
2485
+ /**
2486
+ * Generates JavaScript module for direct import
2487
+ */
2488
+ generateBundleConfigJS(config) {
2489
+ return `/**
2490
+ * Slice.js Bundle Configuration
2491
+ * Generated: ${new Date().toISOString()}
2492
+ * Strategy: ${config.strategy}
2493
+ */
2494
+
2495
+ // Direct bundle configuration (no fetch required)
2496
+ export const SLICE_BUNDLE_CONFIG = ${JSON.stringify(config, null, 2)};
2497
+
2498
+ // Auto-initialization if slice is available
2499
+ if (typeof window !== 'undefined' && window.slice && window.slice.controller) {
2500
+ window.slice.controller.bundleConfig = SLICE_BUNDLE_CONFIG;
2501
+
2502
+ // Load critical bundle automatically
2503
+ if (SLICE_BUNDLE_CONFIG.bundles.critical && !window.slice.controller.criticalBundleLoaded) {
2504
+ (async () => {
2505
+ const bundlePath = "/bundles/" + SLICE_BUNDLE_CONFIG.bundles.critical.file;
2506
+ const integrity = SLICE_BUNDLE_CONFIG.bundles.critical.integrity;
2507
+
2508
+ if (typeof window.slice.controller.verifyBundleIntegrity === 'function') {
2509
+ const ok = await window.slice.controller.verifyBundleIntegrity(bundlePath, integrity);
2510
+ if (!ok) {
2511
+ console.warn('Failed to load critical bundle: integrity check failed');
2512
+ return;
2513
+ }
2514
+ }
2515
+
2516
+ import('./slice-bundle.critical.js').catch(err =>
2517
+ console.warn('Failed to load critical bundle:', err)
2518
+ );
2519
+ window.slice.controller.criticalBundleLoaded = true;
2520
+ })();
2521
+ }
2522
+ }
2523
+ `;
2524
+ }
2525
+ }