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