slicejs-cli 3.0.3 → 3.1.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.
|
@@ -31,11 +31,22 @@ export default class BundleGenerator {
|
|
|
31
31
|
maxCriticalSize: 50 * 1024, // 50KB
|
|
32
32
|
maxCriticalComponents: 15,
|
|
33
33
|
minSharedUsage: 3, // Minimum routes to be considered "shared"
|
|
34
|
+
minVendorSharedUsage: 2,
|
|
35
|
+
minVendorSharedTransformedSize: 2 * 1024,
|
|
34
36
|
maxRouteBundleSize: 120 * 1024,
|
|
35
37
|
maxRouteRequests: 12,
|
|
36
38
|
strategy: 'hybrid' // 'global', 'hybrid', 'per-route'
|
|
37
39
|
};
|
|
38
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
|
+
|
|
39
50
|
this.bundles = {
|
|
40
51
|
critical: {
|
|
41
52
|
components: [],
|
|
@@ -710,6 +721,14 @@ export default class BundleGenerator {
|
|
|
710
721
|
async generateBundleFiles() {
|
|
711
722
|
const files = [];
|
|
712
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
|
+
|
|
713
732
|
// 1. Critical bundle
|
|
714
733
|
if (this.bundles.critical.components.length > 0) {
|
|
715
734
|
const criticalFile = await this.createBundleFile(
|
|
@@ -753,6 +772,148 @@ export default class BundleGenerator {
|
|
|
753
772
|
return files;
|
|
754
773
|
}
|
|
755
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
|
+
|
|
756
917
|
/**
|
|
757
918
|
* Creates a bundle file
|
|
758
919
|
*/
|
|
@@ -992,7 +1153,11 @@ export default class BundleGenerator {
|
|
|
992
1153
|
: this.routeToFileName(routePath || fileName.replace('slice-bundle.', '').replace('.js', ''));
|
|
993
1154
|
|
|
994
1155
|
const dependencyModules = this.collectDependencyModulesFromComponents(uniqueComponents);
|
|
995
|
-
const
|
|
1156
|
+
const isRouteBundle = type === 'route';
|
|
1157
|
+
const dependencyModuleBlock = this.buildV2DependencyModuleBlock(uniqueComponents, {
|
|
1158
|
+
includeSharedResolver: isRouteBundle,
|
|
1159
|
+
omittedDependencies: isRouteBundle ? this.vendorShared.sharedDependencySet : null
|
|
1160
|
+
});
|
|
996
1161
|
const rawHoistedImports = uniqueComponents
|
|
997
1162
|
.flatMap((component) => component.hoistedImports || [])
|
|
998
1163
|
.map((statement) => String(statement).trim())
|
|
@@ -1011,7 +1176,9 @@ export default class BundleGenerator {
|
|
|
1011
1176
|
const classFactoryDefinitions = uniqueComponents
|
|
1012
1177
|
.map((component) => {
|
|
1013
1178
|
const factoryName = this.classFactoryName(component.name);
|
|
1014
|
-
const dependencyBindings = this.buildDependencyBindings(component.externalDependencies || {}
|
|
1179
|
+
const dependencyBindings = this.buildDependencyBindings(component.externalDependencies || {}, {
|
|
1180
|
+
preferShared: isRouteBundle
|
|
1181
|
+
});
|
|
1015
1182
|
const body = component.js && component.js.trim()
|
|
1016
1183
|
? component.js
|
|
1017
1184
|
: `return window.${component.name};`;
|
|
@@ -1073,11 +1240,25 @@ export default class BundleGenerator {
|
|
|
1073
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`;
|
|
1074
1241
|
}
|
|
1075
1242
|
|
|
1076
|
-
buildV2DependencyModuleBlock(components) {
|
|
1243
|
+
buildV2DependencyModuleBlock(components, options = {}) {
|
|
1077
1244
|
const modules = this.collectDependencyModulesFromComponents(components);
|
|
1245
|
+
return this.buildV2DependencyModuleBlockFromModules(modules, options);
|
|
1246
|
+
}
|
|
1078
1247
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1248
|
+
buildV2DependencyModuleBlockFromModules(modules = [], options = {}) {
|
|
1249
|
+
const omittedDependencies = options.omittedDependencies instanceof Set
|
|
1250
|
+
? options.omittedDependencies
|
|
1251
|
+
: new Set(options.omittedDependencies || []);
|
|
1252
|
+
const filteredModules = modules.filter((module) => !omittedDependencies.has(module.name));
|
|
1253
|
+
|
|
1254
|
+
const lines = [
|
|
1255
|
+
'const SLICE_BUNDLE_DEPENDENCIES = {};',
|
|
1256
|
+
...this.getDefaultExportResolverLines()
|
|
1257
|
+
];
|
|
1258
|
+
if (options.includeSharedResolver) {
|
|
1259
|
+
lines.push(...this.getBundleDependencyResolverLines());
|
|
1260
|
+
}
|
|
1261
|
+
filteredModules.forEach((module, index) => {
|
|
1081
1262
|
const exportVar = `__sliceDepExports${index}`;
|
|
1082
1263
|
const transformedContent = this.transformDependencyContent(module.content, exportVar, module.name);
|
|
1083
1264
|
lines.push(`const ${exportVar} = {};`);
|
|
@@ -1237,11 +1418,14 @@ if (window.slice && window.slice.controller) {
|
|
|
1237
1418
|
|
|
1238
1419
|
buildDependencyModuleBlock(componentsData) {
|
|
1239
1420
|
const dependencyModules = this.collectDependencyModules(componentsData);
|
|
1421
|
+
const lines = [
|
|
1422
|
+
'const SLICE_BUNDLE_DEPENDENCIES = {};',
|
|
1423
|
+
...this.getDefaultExportResolverLines()
|
|
1424
|
+
];
|
|
1240
1425
|
if (dependencyModules.length === 0) {
|
|
1241
|
-
return '
|
|
1426
|
+
return `${lines.join('\n')}`;
|
|
1242
1427
|
}
|
|
1243
1428
|
|
|
1244
|
-
const lines = ['const SLICE_BUNDLE_DEPENDENCIES = {};'];
|
|
1245
1429
|
dependencyModules.forEach((module, index) => {
|
|
1246
1430
|
const exportVar = `__sliceDepExports${index}`;
|
|
1247
1431
|
const content = this.transformDependencyContent(module.content, exportVar, module.name);
|
|
@@ -1285,8 +1469,7 @@ if (window.slice && window.slice.controller) {
|
|
|
1285
1469
|
}
|
|
1286
1470
|
|
|
1287
1471
|
transformDependencyContent(content, exportVar, moduleName) {
|
|
1288
|
-
const
|
|
1289
|
-
const dataName = baseName ? `${baseName}Data` : null;
|
|
1472
|
+
const dataName = this.getDependencyDefaultFallbackKey(moduleName);
|
|
1290
1473
|
const exportPrefix = dataName ? `${exportVar}.${dataName} = ` : `${exportVar}.default = `;
|
|
1291
1474
|
|
|
1292
1475
|
return content
|
|
@@ -1308,6 +1491,11 @@ if (window.slice && window.slice.controller) {
|
|
|
1308
1491
|
.replace(/^\s*export\s+/gm, '');
|
|
1309
1492
|
}
|
|
1310
1493
|
|
|
1494
|
+
getDependencyDefaultFallbackKey(moduleName) {
|
|
1495
|
+
const baseName = moduleName?.split('/').pop()?.replace(/\.[^.]+$/, '');
|
|
1496
|
+
return baseName ? `${baseName}Data` : null;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1311
1499
|
buildComponentBundleBlock(componentsData) {
|
|
1312
1500
|
const componentEntries = [];
|
|
1313
1501
|
const componentDefs = [];
|
|
@@ -1344,19 +1532,19 @@ if (window.slice && window.slice.controller) {
|
|
|
1344
1532
|
return `${componentDefs.join('\n\n')}\n\nconst SLICE_BUNDLE_COMPONENTS = {\n${componentEntries.join(',\n')}\n};\n${frameworkBlock}`;
|
|
1345
1533
|
}
|
|
1346
1534
|
|
|
1347
|
-
buildDependencyBindings(externalDependencies) {
|
|
1535
|
+
buildDependencyBindings(externalDependencies, options = {}) {
|
|
1348
1536
|
const lines = [];
|
|
1349
1537
|
Object.entries(externalDependencies).forEach(([name, entry]) => {
|
|
1350
1538
|
const bindings = typeof entry === 'string' ? [] : entry.bindings || [];
|
|
1351
|
-
const depVar =
|
|
1352
|
-
|
|
1353
|
-
|
|
1539
|
+
const depVar = options.preferShared
|
|
1540
|
+
? `__sliceResolveBundleDependency(${JSON.stringify(name)})`
|
|
1541
|
+
: `SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(name)}]`;
|
|
1354
1542
|
|
|
1355
1543
|
bindings.forEach((binding) => {
|
|
1356
1544
|
if (!binding?.localName) return;
|
|
1357
1545
|
if (binding.type === 'default') {
|
|
1358
|
-
const
|
|
1359
|
-
lines.push(`const ${binding.localName} = ${depVar}
|
|
1546
|
+
const preferredKey = this.getDependencyDefaultFallbackKey(name);
|
|
1547
|
+
lines.push(`const ${binding.localName} = __sliceResolveDefaultExport(${depVar}, ${JSON.stringify(name)}, ${JSON.stringify(preferredKey)});`);
|
|
1360
1548
|
}
|
|
1361
1549
|
if (binding.type === 'named') {
|
|
1362
1550
|
lines.push(`const ${binding.localName} = ${depVar}.${binding.importedName};`);
|
|
@@ -1370,6 +1558,41 @@ if (window.slice && window.slice.controller) {
|
|
|
1370
1558
|
return lines.join('\n');
|
|
1371
1559
|
}
|
|
1372
1560
|
|
|
1561
|
+
getBundleDependencyResolverLines() {
|
|
1562
|
+
return [
|
|
1563
|
+
"const __sliceSharedDeps = typeof window !== 'undefined' ? (window.__SLICE_SHARED_DEPS__ || {}) : {};",
|
|
1564
|
+
'const __sliceResolveBundleDependency = (depName) => Object.prototype.hasOwnProperty.call(__sliceSharedDeps, depName) ? __sliceSharedDeps[depName] : SLICE_BUNDLE_DEPENDENCIES[depName];'
|
|
1565
|
+
];
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
getDefaultExportResolverLines() {
|
|
1569
|
+
return [
|
|
1570
|
+
'const __sliceDefaultExportWarningDeps = new Set();',
|
|
1571
|
+
"const __sliceDefaultExportPreferredKeys = ['module', 'exports', 'purify'];",
|
|
1572
|
+
'const __sliceDeterministicKeyCompare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);',
|
|
1573
|
+
'function __sliceResolveDefaultExport(dep, depName, preferredKey) {',
|
|
1574
|
+
' if (dep?.default !== undefined) return dep.default;',
|
|
1575
|
+
" if (dep === null || (typeof dep !== 'object' && typeof dep !== 'function')) return dep;",
|
|
1576
|
+
" if (preferredKey && preferredKey !== 'default' && preferredKey !== '__esModule' && Object.prototype.hasOwnProperty.call(dep, preferredKey)) return dep[preferredKey];",
|
|
1577
|
+
" const keys = Object.keys(dep).filter((key) => key !== 'default' && key !== '__esModule');",
|
|
1578
|
+
' if (keys.length === 1) return dep[keys[0]];',
|
|
1579
|
+
' if (keys.length > 1) {',
|
|
1580
|
+
' const preferredMatches = __sliceDefaultExportPreferredKeys.filter((key) => keys.includes(key));',
|
|
1581
|
+
' if (preferredMatches.length === 1) return dep[preferredMatches[0]];',
|
|
1582
|
+
' const sortedKeys = [...keys].sort(__sliceDeterministicKeyCompare);',
|
|
1583
|
+
' const fallbackKey = sortedKeys[0];',
|
|
1584
|
+
' const warningDepName = depName || "<unknown dependency>";',
|
|
1585
|
+
' if (!__sliceDefaultExportWarningDeps.has(warningDepName)) {',
|
|
1586
|
+
' __sliceDefaultExportWarningDeps.add(warningDepName);',
|
|
1587
|
+
' console.warn(`[Slice.js bundler] Ambiguous default export resolution for "${warningDepName}". Falling back to "${fallbackKey}". Keys: ${sortedKeys.join(\', \')}`);',
|
|
1588
|
+
' }',
|
|
1589
|
+
' return dep[fallbackKey];',
|
|
1590
|
+
' }',
|
|
1591
|
+
' return dep;',
|
|
1592
|
+
'}'
|
|
1593
|
+
];
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1373
1596
|
toSafeIdentifier(name) {
|
|
1374
1597
|
const cleaned = name.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
1375
1598
|
if (/^\d/.test(cleaned)) {
|
|
@@ -1410,6 +1633,17 @@ if (window.slice && window.slice.controller) {
|
|
|
1410
1633
|
integrity: null,
|
|
1411
1634
|
components: []
|
|
1412
1635
|
},
|
|
1636
|
+
vendorShared: {
|
|
1637
|
+
bundleKey: 'vendor-shared',
|
|
1638
|
+
type: 'vendor-shared',
|
|
1639
|
+
file: this.vendorShared.file,
|
|
1640
|
+
size: this.vendorShared.bundle?.size || 0,
|
|
1641
|
+
hash: this.vendorShared.bundle?.hash || null,
|
|
1642
|
+
integrity: this.vendorShared.bundle?.integrity || null,
|
|
1643
|
+
dependencies: Array.from(this.vendorShared.sharedDependencySet).sort((a, b) => a.localeCompare(b)),
|
|
1644
|
+
dependencyCount: this.vendorShared.sharedDependencySet.size,
|
|
1645
|
+
routes: Array.from(this.vendorShared.bundleKeysUsingSharedDependencies).sort((a, b) => a.localeCompare(b))
|
|
1646
|
+
},
|
|
1413
1647
|
critical: {
|
|
1414
1648
|
file: this.bundles.critical.file,
|
|
1415
1649
|
size: this.bundles.critical.size,
|
|
@@ -1419,14 +1653,20 @@ if (window.slice && window.slice.controller) {
|
|
|
1419
1653
|
},
|
|
1420
1654
|
routes: {}
|
|
1421
1655
|
},
|
|
1422
|
-
routeBundles: {}
|
|
1656
|
+
routeBundles: {},
|
|
1657
|
+
routeDependencyGraph: {}
|
|
1423
1658
|
};
|
|
1424
1659
|
|
|
1425
1660
|
for (const [key, bundle] of Object.entries(this.bundles.routes)) {
|
|
1426
1661
|
const routeIdentifier = Array.isArray(bundle.path || bundle.paths)
|
|
1427
1662
|
? key
|
|
1428
1663
|
: (bundle.path || bundle.paths || key);
|
|
1429
|
-
const
|
|
1664
|
+
const usesVendorShared = this.vendorShared.bundleKeysUsingSharedDependencies.has(key)
|
|
1665
|
+
|| (bundle.dependencies || []).includes('vendor-shared');
|
|
1666
|
+
const dependencies = this.mergeBundleDependencies(
|
|
1667
|
+
bundle.dependencies || [],
|
|
1668
|
+
usesVendorShared ? ['vendor-shared'] : []
|
|
1669
|
+
);
|
|
1430
1670
|
|
|
1431
1671
|
config.bundles.routes[key] = {
|
|
1432
1672
|
path: bundle.path || bundle.paths || key, // Support both single path and array of paths, fallback to key
|
|
@@ -1454,6 +1694,27 @@ if (window.slice && window.slice.controller) {
|
|
|
1454
1694
|
if (!config.routeBundles[routePath].includes(key)) {
|
|
1455
1695
|
config.routeBundles[routePath].push(key);
|
|
1456
1696
|
}
|
|
1697
|
+
|
|
1698
|
+
const graphEntry = config.routeDependencyGraph[routePath] || {
|
|
1699
|
+
bundles: [],
|
|
1700
|
+
edges: []
|
|
1701
|
+
};
|
|
1702
|
+
if (!graphEntry.bundles.includes(key)) {
|
|
1703
|
+
graphEntry.bundles.push(key);
|
|
1704
|
+
graphEntry.bundles.sort((a, b) => a.localeCompare(b));
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
const edgeKeys = new Set(graphEntry.edges.map((edge) => `${edge.from}->${edge.to}`));
|
|
1708
|
+
const orderedEdgeSources = ['critical', ...dependencies.filter((dependency) => dependency !== 'critical')];
|
|
1709
|
+
for (const source of orderedEdgeSources) {
|
|
1710
|
+
const edgeKey = `${source}->${key}`;
|
|
1711
|
+
if (!edgeKeys.has(edgeKey)) {
|
|
1712
|
+
graphEntry.edges.push({ from: source, to: key });
|
|
1713
|
+
edgeKeys.add(edgeKey);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
config.routeDependencyGraph[routePath] = graphEntry;
|
|
1457
1718
|
}
|
|
1458
1719
|
}
|
|
1459
1720
|
|
package/package.json
CHANGED
|
@@ -386,3 +386,323 @@ test('formatBundleFile emits hoisted imports for framework-compatible output', (
|
|
|
386
386
|
assert.match(source, /import boot from '\/public\/bootstrap\.js';/);
|
|
387
387
|
assert.match(source, /const SLICE_BUNDLE_DEPENDENCIES = \{\};/);
|
|
388
388
|
});
|
|
389
|
+
|
|
390
|
+
test('default dependency binding resolves transformed default key over named exports', () => {
|
|
391
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
392
|
+
components: [],
|
|
393
|
+
routes: [],
|
|
394
|
+
metrics: {}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const externalDependencies = {
|
|
398
|
+
'App/purify.js': {
|
|
399
|
+
content: 'export default () => "DEFAULT"; export const purify = () => "NAMED";',
|
|
400
|
+
bindings: [{ type: 'default', importedName: 'default', localName: 'purify' }]
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const resolverSource = generator.getDefaultExportResolverLines().join('\n');
|
|
405
|
+
const bindingsSource = generator.buildDependencyBindings(externalDependencies);
|
|
406
|
+
const resolveBoundValue = new Function(
|
|
407
|
+
`${resolverSource}\n` +
|
|
408
|
+
'const SLICE_BUNDLE_DEPENDENCIES = {"App/purify.js": { purifyData: "DEFAULT", purify: "NAMED" }};\n' +
|
|
409
|
+
`${bindingsSource}\n` +
|
|
410
|
+
'return purify;'
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
assert.equal(resolveBoundValue(), 'DEFAULT');
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const evaluateDefaultResolver = ({ dep, depName = 'App/dep.js', preferredKey = null, calls = 1 }) => {
|
|
417
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
418
|
+
components: [],
|
|
419
|
+
routes: [],
|
|
420
|
+
metrics: {}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const resolverSource = generator.getDefaultExportResolverLines().join('\n');
|
|
424
|
+
const resolve = new Function(
|
|
425
|
+
'__dep',
|
|
426
|
+
'__depName',
|
|
427
|
+
'__preferredKey',
|
|
428
|
+
'__calls',
|
|
429
|
+
`${resolverSource}\n` +
|
|
430
|
+
'const __capturedWarnings = [];' +
|
|
431
|
+
'const __originalWarn = console.warn;' +
|
|
432
|
+
'console.warn = (...args) => __capturedWarnings.push(args.join(" "));' +
|
|
433
|
+
'let __result;' +
|
|
434
|
+
'try {' +
|
|
435
|
+
' for (let __i = 0; __i < __calls; __i += 1) {' +
|
|
436
|
+
' __result = __sliceResolveDefaultExport(__dep, __depName, __preferredKey);' +
|
|
437
|
+
' }' +
|
|
438
|
+
'} finally {' +
|
|
439
|
+
' console.warn = __originalWarn;' +
|
|
440
|
+
'}' +
|
|
441
|
+
'return { result: __result, warnings: __capturedWarnings };'
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
return resolve(dep, depName, preferredKey, calls);
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
test('default resolver returns default when present', () => {
|
|
448
|
+
const { result, warnings } = evaluateDefaultResolver({
|
|
449
|
+
dep: { default: 'DEFAULT', alpha: 'ALPHA' }
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
assert.equal(result, 'DEFAULT');
|
|
453
|
+
assert.equal(warnings.length, 0);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('default resolver preserves falsy default values', () => {
|
|
457
|
+
const falsyValues = [0, '', false, null];
|
|
458
|
+
|
|
459
|
+
for (const value of falsyValues) {
|
|
460
|
+
const { result, warnings } = evaluateDefaultResolver({ dep: { default: value, alt: 'fallback' } });
|
|
461
|
+
assert.equal(result, value);
|
|
462
|
+
assert.equal(warnings.length, 0);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test('default resolver falls back to single non-default key', () => {
|
|
467
|
+
const { result, warnings } = evaluateDefaultResolver({ dep: { onlyKey: 42 } });
|
|
468
|
+
|
|
469
|
+
assert.equal(result, 42);
|
|
470
|
+
assert.equal(warnings.length, 0);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test('default resolver respects preferred key hint when present', () => {
|
|
474
|
+
const { result, warnings } = evaluateDefaultResolver({
|
|
475
|
+
dep: { purifyData: 'PREFERRED', purify: 'OTHER' },
|
|
476
|
+
depName: 'App/purify.js',
|
|
477
|
+
preferredKey: 'purifyData'
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
assert.equal(result, 'PREFERRED');
|
|
481
|
+
assert.equal(warnings.length, 0);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test('default resolver prefers known keys module/exports/purify when unambiguous', () => {
|
|
485
|
+
const moduleResult = evaluateDefaultResolver({ dep: { module: 'MODULE', alpha: 'A' } });
|
|
486
|
+
const exportsResult = evaluateDefaultResolver({ dep: { exports: 'EXPORTS', beta: 'B' } });
|
|
487
|
+
const purifyResult = evaluateDefaultResolver({ dep: { purify: 'PURIFY', gamma: 'C' } });
|
|
488
|
+
|
|
489
|
+
assert.equal(moduleResult.result, 'MODULE');
|
|
490
|
+
assert.equal(exportsResult.result, 'EXPORTS');
|
|
491
|
+
assert.equal(purifyResult.result, 'PURIFY');
|
|
492
|
+
assert.equal(moduleResult.warnings.length, 0);
|
|
493
|
+
assert.equal(exportsResult.warnings.length, 0);
|
|
494
|
+
assert.equal(purifyResult.warnings.length, 0);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test('default resolver uses deterministic alphabetical fallback on ambiguous keys', () => {
|
|
498
|
+
const { result } = evaluateDefaultResolver({ dep: { zebra: 'Z', alpha: 'A', middle: 'M' } });
|
|
499
|
+
assert.equal(result, 'A');
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test('default resolver source uses locale-independent comparator and named preferred keys constant', () => {
|
|
503
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
504
|
+
components: [],
|
|
505
|
+
routes: [],
|
|
506
|
+
metrics: {}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
const resolverSource = generator.getDefaultExportResolverLines().join('\n');
|
|
510
|
+
|
|
511
|
+
assert.match(resolverSource, /const __sliceDefaultExportPreferredKeys = \['module', 'exports', 'purify'\];/);
|
|
512
|
+
assert.match(resolverSource, /const __sliceDeterministicKeyCompare = \(a, b\) => \(a < b \? -1 : a > b \? 1 : 0\);/);
|
|
513
|
+
assert.doesNotMatch(resolverSource, /localeCompare/);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test('default resolver warning includes dependency path keys and chosen key', () => {
|
|
517
|
+
const { warnings } = evaluateDefaultResolver({
|
|
518
|
+
dep: { zebra: 'Z', alpha: 'A', middle: 'M' },
|
|
519
|
+
depName: 'App/ambiguous.js'
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
assert.equal(warnings.length, 1);
|
|
523
|
+
assert.match(warnings[0], /App\/ambiguous\.js/);
|
|
524
|
+
assert.match(warnings[0], /Falling back to "alpha"/);
|
|
525
|
+
assert.match(warnings[0], /Keys: alpha, middle, zebra/);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test('default resolver deduplicates ambiguous warning per dependency in one evaluation', () => {
|
|
529
|
+
const { warnings } = evaluateDefaultResolver({
|
|
530
|
+
dep: { c: 3, a: 1, b: 2 },
|
|
531
|
+
depName: 'App/repeat-warning.js',
|
|
532
|
+
calls: 3
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
assert.equal(warnings.length, 1);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test('default resolver evaluation restores console.warn when resolver throws', () => {
|
|
539
|
+
const originalWarn = console.warn;
|
|
540
|
+
|
|
541
|
+
assert.throws(() => {
|
|
542
|
+
evaluateDefaultResolver({
|
|
543
|
+
dep: new Proxy({}, {
|
|
544
|
+
ownKeys() {
|
|
545
|
+
throw new Error('kaboom');
|
|
546
|
+
}
|
|
547
|
+
})
|
|
548
|
+
});
|
|
549
|
+
}, /kaboom/);
|
|
550
|
+
|
|
551
|
+
assert.equal(console.warn, originalWarn);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test('default resolver passes through non-object values and null/undefined', () => {
|
|
555
|
+
const fn = () => 'ok';
|
|
556
|
+
const functionResult = evaluateDefaultResolver({ dep: fn });
|
|
557
|
+
const stringResult = evaluateDefaultResolver({ dep: 'value' });
|
|
558
|
+
const numberResult = evaluateDefaultResolver({ dep: 7 });
|
|
559
|
+
const nullResult = evaluateDefaultResolver({ dep: null });
|
|
560
|
+
const undefinedResult = evaluateDefaultResolver({ dep: undefined });
|
|
561
|
+
|
|
562
|
+
assert.equal(functionResult.result, fn);
|
|
563
|
+
assert.equal(stringResult.result, 'value');
|
|
564
|
+
assert.equal(numberResult.result, 7);
|
|
565
|
+
assert.equal(nullResult.result, null);
|
|
566
|
+
assert.equal(undefinedResult.result, undefined);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test('analyzeDependencies resolves extensionless imports across js json and mjs', async () => {
|
|
570
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'slice-deps-ext-'));
|
|
571
|
+
const componentDir = path.join(tempRoot, 'Component');
|
|
572
|
+
|
|
573
|
+
await fs.ensureDir(componentDir);
|
|
574
|
+
await fs.writeFile(path.join(componentDir, 'dep-js.js'), 'export const value = 1;', 'utf-8');
|
|
575
|
+
await fs.writeFile(path.join(componentDir, 'dep-json.json'), '{"ok":true}', 'utf-8');
|
|
576
|
+
await fs.writeFile(path.join(componentDir, 'dep-mjs.mjs'), 'export const ok = true;', 'utf-8');
|
|
577
|
+
|
|
578
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
579
|
+
components: [],
|
|
580
|
+
routes: [],
|
|
581
|
+
metrics: {}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
const jsDeps = generator.analyzeDependencies("import dep from './dep-js';", componentDir);
|
|
586
|
+
const jsonDeps = generator.analyzeDependencies("import cfg from './dep-json';", componentDir);
|
|
587
|
+
const mjsDeps = generator.analyzeDependencies("import mod from './dep-mjs';", componentDir);
|
|
588
|
+
|
|
589
|
+
assert.equal(jsDeps.length, 1);
|
|
590
|
+
assert.equal(path.basename(jsDeps[0].path), 'dep-js.js');
|
|
591
|
+
assert.equal(jsonDeps.length, 1);
|
|
592
|
+
assert.equal(path.basename(jsonDeps[0].path), 'dep-json.json');
|
|
593
|
+
assert.equal(mjsDeps.length, 1);
|
|
594
|
+
assert.equal(path.basename(mjsDeps[0].path), 'dep-mjs.mjs');
|
|
595
|
+
} finally {
|
|
596
|
+
await fs.remove(tempRoot);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test('named and namespace dependency bindings remain unchanged', () => {
|
|
601
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
602
|
+
components: [],
|
|
603
|
+
routes: [],
|
|
604
|
+
metrics: {}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
const externalDependencies = {
|
|
608
|
+
'App/named.js': {
|
|
609
|
+
content: 'export const alpha = 1;',
|
|
610
|
+
bindings: [
|
|
611
|
+
{ type: 'named', importedName: 'alpha', localName: 'localAlpha' },
|
|
612
|
+
{ type: 'namespace', localName: 'namedNamespace' }
|
|
613
|
+
]
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const resolverSource = generator.getDefaultExportResolverLines().join('\n');
|
|
618
|
+
const bindingsSource = generator.buildDependencyBindings(externalDependencies);
|
|
619
|
+
const evaluate = new Function(
|
|
620
|
+
`${resolverSource}\n` +
|
|
621
|
+
'const SLICE_BUNDLE_DEPENDENCIES = {"App/named.js": { alpha: 99, other: 1 }};' +
|
|
622
|
+
`${bindingsSource}\n` +
|
|
623
|
+
'return { localAlpha, namedNamespace };'
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
const values = evaluate();
|
|
627
|
+
assert.equal(values.localAlpha, 99);
|
|
628
|
+
assert.deepEqual(values.namedNamespace, { alpha: 99, other: 1 });
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
test('indexExternalDependencyUsage tracks unique route-bundle usage counts', () => {
|
|
632
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
633
|
+
components: [],
|
|
634
|
+
routes: [],
|
|
635
|
+
metrics: {}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
const routeDependencyIndex = {
|
|
639
|
+
alpha: {
|
|
640
|
+
'deps/shared.js': { content: 'export const shared = true;' },
|
|
641
|
+
'deps/alpha.js': { content: 'export const alpha = true;' }
|
|
642
|
+
},
|
|
643
|
+
beta: {
|
|
644
|
+
'deps/shared.js': { content: 'export const shared = true;' },
|
|
645
|
+
'deps/beta.js': { content: 'export const beta = true;' }
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const usageIndex = generator.indexExternalDependencyUsage(routeDependencyIndex);
|
|
650
|
+
|
|
651
|
+
assert.equal(usageIndex.get('deps/shared.js').bundleCount, 2);
|
|
652
|
+
assert.equal(usageIndex.get('deps/alpha.js').bundleCount, 1);
|
|
653
|
+
assert.deepEqual(Array.from(usageIndex.get('deps/shared.js').bundleKeys).sort(), ['alpha', 'beta']);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test('computeSharedDependencySet enforces usage and transformed-size thresholds', () => {
|
|
657
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
658
|
+
components: [],
|
|
659
|
+
routes: [],
|
|
660
|
+
metrics: {}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
const largePayload = 'x'.repeat(2100);
|
|
664
|
+
const routeDependencyIndex = {
|
|
665
|
+
alpha: {
|
|
666
|
+
'deps/shared-large.js': { content: `export const payload = '${largePayload}';` },
|
|
667
|
+
'deps/shared-small.js': { content: 'export const tiny = 1;' }
|
|
668
|
+
},
|
|
669
|
+
beta: {
|
|
670
|
+
'deps/shared-large.js': { content: `export const payload = '${largePayload}';` },
|
|
671
|
+
'deps/shared-small.js': { content: 'export const tiny = 1;' }
|
|
672
|
+
},
|
|
673
|
+
gamma: {
|
|
674
|
+
'deps/shared-small.js': { content: 'export const tiny = 1;' }
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
const usageIndex = generator.indexExternalDependencyUsage(routeDependencyIndex);
|
|
679
|
+
const sharedSet = generator.computeSharedDependencySet(usageIndex);
|
|
680
|
+
|
|
681
|
+
assert.ok(sharedSet.has('deps/shared-large.js'));
|
|
682
|
+
assert.ok(!sharedSet.has('deps/shared-small.js'));
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test('generateVendorSharedDependencyBundleContent emits shared dependency module once', () => {
|
|
686
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
687
|
+
components: [],
|
|
688
|
+
routes: [],
|
|
689
|
+
metrics: {}
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
const routeDependencyIndex = {
|
|
693
|
+
alpha: {
|
|
694
|
+
'deps/shared.js': { content: `export const payload = '${'y'.repeat(2200)}';` }
|
|
695
|
+
},
|
|
696
|
+
beta: {
|
|
697
|
+
'deps/shared.js': { content: `export const payload = '${'y'.repeat(2200)}';` }
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
const usageIndex = generator.indexExternalDependencyUsage(routeDependencyIndex);
|
|
702
|
+
const sharedSet = generator.computeSharedDependencySet(usageIndex);
|
|
703
|
+
generator.vendorShared.dependencyUsage = usageIndex;
|
|
704
|
+
const content = generator.generateVendorSharedDependencyBundleContent(sharedSet);
|
|
705
|
+
const assignmentMatches = content.match(/SLICE_BUNDLE_DEPENDENCIES\["deps\/shared\.js"\]/g) || [];
|
|
706
|
+
|
|
707
|
+
assert.equal(assignmentMatches.length, 1);
|
|
708
|
+
});
|
|
@@ -129,6 +129,10 @@ test('bundle output inlines dependency modules and binds imported symbols in cla
|
|
|
129
129
|
'App/documentationRoutes.js': {
|
|
130
130
|
content: 'export const documentationRoutes = ["/docs"];',
|
|
131
131
|
bindings: [{ type: 'named', importedName: 'documentationRoutes', localName: 'documentationRoutes' }]
|
|
132
|
+
},
|
|
133
|
+
'App/purify.js': {
|
|
134
|
+
content: 'export const purify = (value) => value;',
|
|
135
|
+
bindings: [{ type: 'default', importedName: 'default', localName: 'purify' }]
|
|
132
136
|
}
|
|
133
137
|
}
|
|
134
138
|
}],
|
|
@@ -136,8 +140,180 @@ test('bundle output inlines dependency modules and binds imported symbols in cla
|
|
|
136
140
|
);
|
|
137
141
|
|
|
138
142
|
assert.match(source, /const SLICE_BUNDLE_DEPENDENCIES = \{\};/);
|
|
143
|
+
assert.match(source, /const __sliceSharedDeps = typeof window !== 'undefined' \? \(window\.__SLICE_SHARED_DEPS__ \|\| \{\}\) : \{\};/);
|
|
144
|
+
assert.match(source, /function __sliceResolveDefaultExport\(dep, depName, preferredKey\) \{/);
|
|
139
145
|
assert.match(source, /SLICE_BUNDLE_DEPENDENCIES\["App\/documentationRoutes\.js"\] = __sliceDepExports0;/);
|
|
140
|
-
assert.match(source, /const documentationRoutes =
|
|
146
|
+
assert.match(source, /const documentationRoutes = __sliceResolveBundleDependency\("App\/documentationRoutes\.js"\)\.documentationRoutes;/);
|
|
147
|
+
assert.match(source, /const purify = __sliceResolveDefaultExport\(__sliceResolveBundleDependency\("App\/purify\.js"\), "App\/purify\.js", "purifyData"\);/);
|
|
148
|
+
assert.doesNotMatch(source, /\.default !== undefined \?/);
|
|
149
|
+
assert.doesNotMatch(source, /SLICE_BUNDLE_DEPENDENCIES\["App\/purify\.js"\]\.purifyData/);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('route bundle omits extracted vendor-shared modules from inline dependency block and keeps bindings', () => {
|
|
153
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
154
|
+
components: [],
|
|
155
|
+
routes: [],
|
|
156
|
+
metrics: {
|
|
157
|
+
totalComponents: 0,
|
|
158
|
+
totalRoutes: 0,
|
|
159
|
+
sharedPercentage: 0,
|
|
160
|
+
totalSize: 0
|
|
161
|
+
}
|
|
162
|
+
}, { output: 'src' });
|
|
163
|
+
|
|
164
|
+
generator.vendorShared.sharedDependencySet = new Set(['App/documentationRoutes.js']);
|
|
165
|
+
|
|
166
|
+
const source = generator.generateBundleFileContent(
|
|
167
|
+
'slice-bundle.test.js',
|
|
168
|
+
'route',
|
|
169
|
+
[{
|
|
170
|
+
name: 'DocumentationPage',
|
|
171
|
+
category: 'AppComponents',
|
|
172
|
+
categoryType: 'Visual',
|
|
173
|
+
dependencies: new Set(),
|
|
174
|
+
size: 100,
|
|
175
|
+
js: 'class DocumentationPage extends HTMLElement { connectedCallback(){ return [documentationRoutes.length, docsNs.documentationRoutes.length, purify(1)].length; } }\nwindow.DocumentationPage = DocumentationPage;\nreturn DocumentationPage;',
|
|
176
|
+
html: '',
|
|
177
|
+
css: '',
|
|
178
|
+
externalDependencies: {
|
|
179
|
+
'App/documentationRoutes.js': {
|
|
180
|
+
content: 'export const documentationRoutes = ["/docs"];',
|
|
181
|
+
bindings: [
|
|
182
|
+
{ type: 'named', importedName: 'documentationRoutes', localName: 'documentationRoutes' },
|
|
183
|
+
{ type: 'namespace', localName: 'docsNs' }
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
'App/purify.js': {
|
|
187
|
+
content: 'export default (value) => value;',
|
|
188
|
+
bindings: [{ type: 'default', importedName: 'default', localName: 'purify' }]
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}],
|
|
192
|
+
'/docs'
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
assert.match(source, /const __sliceSharedDeps = typeof window !== 'undefined' \? \(window\.__SLICE_SHARED_DEPS__ \|\| \{\}\) : \{\};/);
|
|
196
|
+
assert.match(source, /const __sliceResolveBundleDependency = \(depName\) => Object\.prototype\.hasOwnProperty\.call\(__sliceSharedDeps, depName\) \? __sliceSharedDeps\[depName\] : SLICE_BUNDLE_DEPENDENCIES\[depName\];/);
|
|
197
|
+
assert.doesNotMatch(source, /SLICE_BUNDLE_DEPENDENCIES\["App\/documentationRoutes\.js"\] = __sliceDepExports\d+;/);
|
|
198
|
+
assert.match(source, /SLICE_BUNDLE_DEPENDENCIES\["App\/purify\.js"\] = __sliceDepExports\d+;/);
|
|
199
|
+
assert.match(source, /const documentationRoutes = __sliceResolveBundleDependency\("App\/documentationRoutes\.js"\)\.documentationRoutes;/);
|
|
200
|
+
assert.match(source, /const docsNs = __sliceResolveBundleDependency\("App\/documentationRoutes\.js"\);/);
|
|
201
|
+
assert.match(source, /const purify = __sliceResolveDefaultExport\(__sliceResolveBundleDependency\("App\/purify\.js"\), "App\/purify\.js", "purifyData"\);/);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('route bundle emits guarded shared deps resolver for non-browser contexts', () => {
|
|
205
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
206
|
+
components: [],
|
|
207
|
+
routes: [],
|
|
208
|
+
metrics: {
|
|
209
|
+
totalComponents: 0,
|
|
210
|
+
totalRoutes: 0,
|
|
211
|
+
sharedPercentage: 0,
|
|
212
|
+
totalSize: 0
|
|
213
|
+
}
|
|
214
|
+
}, { output: 'src' });
|
|
215
|
+
|
|
216
|
+
const source = generator.generateBundleFileContent(
|
|
217
|
+
'slice-bundle.test.js',
|
|
218
|
+
'route',
|
|
219
|
+
[{
|
|
220
|
+
name: 'DocsPage',
|
|
221
|
+
category: 'AppComponents',
|
|
222
|
+
categoryType: 'Visual',
|
|
223
|
+
dependencies: new Set(),
|
|
224
|
+
size: 100,
|
|
225
|
+
js: 'class DocsPage extends HTMLElement {}\nwindow.DocsPage = DocsPage;\nreturn DocsPage;',
|
|
226
|
+
html: '',
|
|
227
|
+
css: '',
|
|
228
|
+
externalDependencies: {
|
|
229
|
+
'App/deps.js': {
|
|
230
|
+
content: 'export const value = 1;',
|
|
231
|
+
bindings: [{ type: 'named', importedName: 'value', localName: 'value' }]
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}],
|
|
235
|
+
'/docs'
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
assert.match(source, /const __sliceSharedDeps = typeof window !== 'undefined' \? \(window\.__SLICE_SHARED_DEPS__ \|\| \{\}\) : \{\};/);
|
|
239
|
+
assert.match(source, /const __sliceResolveBundleDependency = \(depName\) => Object\.prototype\.hasOwnProperty\.call\(__sliceSharedDeps, depName\) \? __sliceSharedDeps\[depName\] : SLICE_BUNDLE_DEPENDENCIES\[depName\];/);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('non-route bundle does not emit shared resolver block', () => {
|
|
243
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
244
|
+
components: [],
|
|
245
|
+
routes: [],
|
|
246
|
+
metrics: {
|
|
247
|
+
totalComponents: 0,
|
|
248
|
+
totalRoutes: 0,
|
|
249
|
+
sharedPercentage: 0,
|
|
250
|
+
totalSize: 0
|
|
251
|
+
}
|
|
252
|
+
}, { output: 'src' });
|
|
253
|
+
|
|
254
|
+
const source = generator.generateBundleFileContent(
|
|
255
|
+
'slice-bundle.framework.js',
|
|
256
|
+
'framework',
|
|
257
|
+
[{
|
|
258
|
+
name: 'FrameworkComp',
|
|
259
|
+
category: 'Framework',
|
|
260
|
+
categoryType: 'Visual',
|
|
261
|
+
dependencies: new Set(),
|
|
262
|
+
size: 100,
|
|
263
|
+
js: 'class FrameworkComp extends HTMLElement {}\nwindow.FrameworkComp = FrameworkComp;\nreturn FrameworkComp;',
|
|
264
|
+
html: '',
|
|
265
|
+
css: '',
|
|
266
|
+
externalDependencies: {
|
|
267
|
+
'App/frameworkDep.js': {
|
|
268
|
+
content: 'export const dep = 1;',
|
|
269
|
+
bindings: [{ type: 'named', importedName: 'dep', localName: 'dep' }]
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}],
|
|
273
|
+
null
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
assert.doesNotMatch(source, /const __sliceSharedDeps = /);
|
|
277
|
+
assert.doesNotMatch(source, /const __sliceResolveBundleDependency = /);
|
|
278
|
+
assert.match(source, /const dep = SLICE_BUNDLE_DEPENDENCIES\["App\/frameworkDep\.js"\]\.dep;/);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('route bundle keeps local fallback binding when shared map is unavailable', () => {
|
|
282
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
283
|
+
components: [],
|
|
284
|
+
routes: [],
|
|
285
|
+
metrics: {
|
|
286
|
+
totalComponents: 0,
|
|
287
|
+
totalRoutes: 0,
|
|
288
|
+
sharedPercentage: 0,
|
|
289
|
+
totalSize: 0
|
|
290
|
+
}
|
|
291
|
+
}, { output: 'src' });
|
|
292
|
+
|
|
293
|
+
const source = generator.generateBundleFileContent(
|
|
294
|
+
'slice-bundle.test.js',
|
|
295
|
+
'route',
|
|
296
|
+
[{
|
|
297
|
+
name: 'FallbackComp',
|
|
298
|
+
category: 'Visual',
|
|
299
|
+
categoryType: 'Visual',
|
|
300
|
+
dependencies: new Set(),
|
|
301
|
+
size: 100,
|
|
302
|
+
js: 'class FallbackComp extends HTMLElement {}\nwindow.FallbackComp = FallbackComp;\nreturn FallbackComp;',
|
|
303
|
+
html: '',
|
|
304
|
+
css: '',
|
|
305
|
+
externalDependencies: {
|
|
306
|
+
'App/local.js': {
|
|
307
|
+
content: 'export const local = 1;',
|
|
308
|
+
bindings: [{ type: 'named', importedName: 'local', localName: 'local' }]
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}],
|
|
312
|
+
'/fallback'
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
assert.match(source, /const __sliceResolveBundleDependency = \(depName\) => Object\.prototype\.hasOwnProperty\.call\(__sliceSharedDeps, depName\) \? __sliceSharedDeps\[depName\] : SLICE_BUNDLE_DEPENDENCIES\[depName\];/);
|
|
316
|
+
assert.match(source, /const local = __sliceResolveBundleDependency\("App\/local\.js"\)\.local;/);
|
|
141
317
|
});
|
|
142
318
|
|
|
143
319
|
test('bundle output hoists allowed absolute imports to module top-level', () => {
|
|
@@ -249,3 +425,46 @@ test('generateBundleFileContent throws on reserved identifier collision', () =>
|
|
|
249
425
|
);
|
|
250
426
|
}, /reserved identifier collision: SLICE_BUNDLE_META/);
|
|
251
427
|
});
|
|
428
|
+
|
|
429
|
+
test('generateBundleConfig emits vendor-shared metadata and route dependency graph edges', () => {
|
|
430
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
431
|
+
components: [],
|
|
432
|
+
routes: [],
|
|
433
|
+
metrics: {
|
|
434
|
+
totalComponents: 0,
|
|
435
|
+
totalRoutes: 1,
|
|
436
|
+
sharedPercentage: 0,
|
|
437
|
+
totalSize: 0
|
|
438
|
+
}
|
|
439
|
+
}, { output: 'src' });
|
|
440
|
+
|
|
441
|
+
generator.bundles.routes = {
|
|
442
|
+
docs: {
|
|
443
|
+
path: '/docs',
|
|
444
|
+
components: [{ name: 'DocumentationPage' }],
|
|
445
|
+
size: 100,
|
|
446
|
+
file: 'slice-bundle.docs.js'
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
generator.vendorShared.sharedDependencySet = new Set(['App/documentationRoutes.js']);
|
|
451
|
+
generator.vendorShared.bundleKeysUsingSharedDependencies = new Set(['docs']);
|
|
452
|
+
generator.vendorShared.bundle = {
|
|
453
|
+
file: 'slice-bundle.vendor-shared.js',
|
|
454
|
+
size: 2048,
|
|
455
|
+
hash: 'deadbeef',
|
|
456
|
+
integrity: 'sha256:deadbeef'
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const config = generator.generateBundleConfig(null);
|
|
460
|
+
|
|
461
|
+
assert.equal(config.bundles.vendorShared.bundleKey, 'vendor-shared');
|
|
462
|
+
assert.equal(config.bundles.vendorShared.type, 'vendor-shared');
|
|
463
|
+
assert.equal(config.bundles.vendorShared.dependencyCount, 1);
|
|
464
|
+
assert.deepEqual(config.bundles.routes.docs.dependencies, ['critical', 'vendor-shared']);
|
|
465
|
+
assert.deepEqual(config.routeBundles['/docs'], ['critical', 'vendor-shared', 'docs']);
|
|
466
|
+
assert.deepEqual(config.routeDependencyGraph['/docs'].edges, [
|
|
467
|
+
{ from: 'critical', to: 'docs' },
|
|
468
|
+
{ from: 'vendor-shared', to: 'docs' }
|
|
469
|
+
]);
|
|
470
|
+
});
|