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 dependencyModuleBlock = this.buildV2DependencyModuleBlock(uniqueComponents);
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
- const lines = ['const SLICE_BUNDLE_DEPENDENCIES = {};'];
1080
- modules.forEach((module, index) => {
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 'const SLICE_BUNDLE_DEPENDENCIES = {};';
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 baseName = moduleName.split('/').pop().replace(/\.[^.]+$/, '');
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 = `SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(name)}]`;
1352
- const baseName = name.split('/').pop().replace(/\.[^.]+$/, '');
1353
- const dataName = baseName ? `${baseName}Data` : null;
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 fallback = dataName ? `${depVar}.${dataName}` : `${depVar}.default`;
1359
- lines.push(`const ${binding.localName} = ${depVar}.default !== undefined ? ${depVar}.default : ${fallback};`);
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 dependencies = this.mergeBundleDependencies(bundle.dependencies || []);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slicejs-cli",
3
- "version": "3.0.3",
3
+ "version": "3.1.0",
4
4
  "description": "Command client for developing web applications with Slice.js framework",
5
5
  "main": "client.js",
6
6
  "bin": {
@@ -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 = SLICE_BUNDLE_DEPENDENCIES\["App\/documentationRoutes\.js"\]\.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
+ });