slicejs-web-framework 2.4.7 → 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.
Files changed (115) hide show
  1. package/.worktrees/bundling-v2-precompiled-registrars/LICENSE +21 -0
  2. package/.worktrees/bundling-v2-precompiled-registrars/Slice/Components/Structural/ContextManager/ContextManager.js +369 -0
  3. package/.worktrees/bundling-v2-precompiled-registrars/Slice/Components/Structural/ContextManager/ContextManagerDebugger.js +297 -0
  4. package/.worktrees/bundling-v2-precompiled-registrars/Slice/Components/Structural/Controller/Controller.js +972 -0
  5. package/.worktrees/bundling-v2-precompiled-registrars/Slice/Components/Structural/Debugger/Debugger.css +620 -0
  6. package/.worktrees/bundling-v2-precompiled-registrars/Slice/Components/Structural/Debugger/Debugger.html +73 -0
  7. package/.worktrees/bundling-v2-precompiled-registrars/Slice/Components/Structural/Debugger/Debugger.js +1548 -0
  8. package/.worktrees/bundling-v2-precompiled-registrars/Slice/Components/Structural/EventManager/EventManager.js +338 -0
  9. package/.worktrees/bundling-v2-precompiled-registrars/Slice/Components/Structural/EventManager/EventManagerDebugger.js +361 -0
  10. package/.worktrees/bundling-v2-precompiled-registrars/Slice/Components/Structural/Logger/Log.js +10 -0
  11. package/.worktrees/bundling-v2-precompiled-registrars/Slice/Components/Structural/Logger/Logger.js +146 -0
  12. package/.worktrees/bundling-v2-precompiled-registrars/Slice/Components/Structural/Router/Router.js +721 -0
  13. package/.worktrees/bundling-v2-precompiled-registrars/Slice/Components/Structural/StylesManager/StylesManager.js +78 -0
  14. package/.worktrees/bundling-v2-precompiled-registrars/Slice/Components/Structural/StylesManager/ThemeManager/ThemeManager.js +84 -0
  15. package/.worktrees/bundling-v2-precompiled-registrars/Slice/Slice.js +504 -0
  16. package/.worktrees/bundling-v2-precompiled-registrars/Slice/tests/bundle-v2-runtime-contract.test.js +268 -0
  17. package/.worktrees/bundling-v2-precompiled-registrars/Slice/tests/router-loading-finally.test.js +68 -0
  18. package/.worktrees/bundling-v2-precompiled-registrars/api/index.js +286 -0
  19. package/.worktrees/bundling-v2-precompiled-registrars/api/middleware/securityMiddleware.js +253 -0
  20. package/.worktrees/bundling-v2-precompiled-registrars/package.json +37 -0
  21. package/.worktrees/bundling-v2-precompiled-registrars/sliceConfig.schema.json +109 -0
  22. package/.worktrees/bundling-v2-precompiled-registrars/src/App/index.html +22 -0
  23. package/.worktrees/bundling-v2-precompiled-registrars/src/App/index.js +23 -0
  24. package/.worktrees/bundling-v2-precompiled-registrars/src/App/style.css +40 -0
  25. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/AppComponents/HomePage/HomePage.css +201 -0
  26. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/AppComponents/HomePage/HomePage.html +37 -0
  27. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/AppComponents/HomePage/HomePage.js +210 -0
  28. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/AppComponents/Playground/Playground.css +12 -0
  29. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/AppComponents/Playground/Playground.html +0 -0
  30. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/AppComponents/Playground/Playground.js +111 -0
  31. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Service/FetchManager/FetchManager.js +133 -0
  32. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Service/IndexedDbManager/IndexedDbManager.js +141 -0
  33. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Service/LocalStorageManager/LocalStorageManager.js +45 -0
  34. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Button/Button.css +47 -0
  35. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Button/Button.html +5 -0
  36. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Button/Button.js +93 -0
  37. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Card/Card.css +68 -0
  38. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Card/Card.html +7 -0
  39. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Card/Card.js +107 -0
  40. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Checkbox/Checkbox.css +87 -0
  41. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Checkbox/Checkbox.html +8 -0
  42. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Checkbox/Checkbox.js +86 -0
  43. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/CodeVisualizer/CodeVisualizer.css +130 -0
  44. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/CodeVisualizer/CodeVisualizer.html +4 -0
  45. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/CodeVisualizer/CodeVisualizer.js +262 -0
  46. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Details/Details.css +70 -0
  47. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Details/Details.html +9 -0
  48. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Details/Details.js +76 -0
  49. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/DropDown/DropDown.css +60 -0
  50. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/DropDown/DropDown.html +5 -0
  51. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/DropDown/DropDown.js +63 -0
  52. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Grid/Grid.css +7 -0
  53. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Grid/Grid.html +1 -0
  54. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Grid/Grid.js +57 -0
  55. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Icon/Icon.css +510 -0
  56. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Icon/Icon.html +1 -0
  57. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Icon/Icon.js +89 -0
  58. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Icon/slc.eot +0 -0
  59. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Icon/slc.json +555 -0
  60. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Icon/slc.styl +507 -0
  61. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Icon/slc.svg +1485 -0
  62. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Icon/slc.symbol.svg +1059 -0
  63. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Icon/slc.ttf +0 -0
  64. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Icon/slc.woff +0 -0
  65. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Icon/slc.woff2 +0 -0
  66. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Input/Input.css +91 -0
  67. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Input/Input.html +4 -0
  68. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Input/Input.js +215 -0
  69. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Layout/Layout.css +0 -0
  70. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Layout/Layout.html +0 -0
  71. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Layout/Layout.js +49 -0
  72. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Link/Link.css +8 -0
  73. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Link/Link.html +1 -0
  74. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Link/Link.js +63 -0
  75. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Loading/Loading.css +56 -0
  76. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Loading/Loading.html +83 -0
  77. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Loading/Loading.js +38 -0
  78. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/MultiRoute/MultiRoute.js +93 -0
  79. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Navbar/Navbar.css +115 -0
  80. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Navbar/Navbar.html +44 -0
  81. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Navbar/Navbar.js +141 -0
  82. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/NotFound/NotFound.css +117 -0
  83. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/NotFound/NotFound.html +24 -0
  84. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/NotFound/NotFound.js +16 -0
  85. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Route/Route.js +93 -0
  86. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Select/Select.css +84 -0
  87. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Select/Select.html +8 -0
  88. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Select/Select.js +195 -0
  89. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Switch/Switch.css +76 -0
  90. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Switch/Switch.html +8 -0
  91. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/Switch/Switch.js +102 -0
  92. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/TreeItem/TreeItem.css +36 -0
  93. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/TreeItem/TreeItem.html +1 -0
  94. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/TreeItem/TreeItem.js +126 -0
  95. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/TreeView/TreeView.css +8 -0
  96. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/TreeView/TreeView.html +1 -0
  97. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/Visual/TreeView/TreeView.js +48 -0
  98. package/.worktrees/bundling-v2-precompiled-registrars/src/Components/components.js +27 -0
  99. package/.worktrees/bundling-v2-precompiled-registrars/src/Styles/sliceStyles.css +34 -0
  100. package/.worktrees/bundling-v2-precompiled-registrars/src/Themes/Dark.css +42 -0
  101. package/.worktrees/bundling-v2-precompiled-registrars/src/Themes/Light.css +31 -0
  102. package/.worktrees/bundling-v2-precompiled-registrars/src/Themes/Slice.css +47 -0
  103. package/.worktrees/bundling-v2-precompiled-registrars/src/images/Slice.js-logo.png +0 -0
  104. package/.worktrees/bundling-v2-precompiled-registrars/src/images/favicon.ico +0 -0
  105. package/.worktrees/bundling-v2-precompiled-registrars/src/images/im2/Slice.js-logo.png +0 -0
  106. package/.worktrees/bundling-v2-precompiled-registrars/src/routes.js +16 -0
  107. package/.worktrees/bundling-v2-precompiled-registrars/src/sliceConfig.json +73 -0
  108. package/.worktrees/bundling-v2-precompiled-registrars/src/testing.js +888 -0
  109. package/Slice/Components/Structural/Controller/Controller.js +77 -31
  110. package/Slice/Components/Structural/Router/Router.js +25 -24
  111. package/Slice/Components/Structural/StylesManager/StylesManager.js +15 -5
  112. package/Slice/Slice.js +60 -63
  113. package/Slice/tests/bundle-v2-runtime-contract.test.js +268 -0
  114. package/Slice/tests/router-loading-finally.test.js +68 -0
  115. package/package.json +1 -1
@@ -16,6 +16,7 @@ export default class Controller {
16
16
  this.loadedBundles = new Set();
17
17
  this.bundleConfig = null;
18
18
  this.criticalBundleLoaded = false;
19
+ this.bundleImportPromises = new Map();
19
20
 
20
21
  this.idCounter = 0;
21
22
  }
@@ -36,9 +37,53 @@ export default class Controller {
36
37
  // No bundles available, will use individual component loading
37
38
  this.bundleConfig = null;
38
39
  this.criticalBundleLoaded = false;
39
- }
40
+ }
40
41
  }
41
42
 
43
+ /**
44
+ * Import a bundle URL once per page session.
45
+ * Reuses the same Promise for concurrent callers.
46
+ * @param {string} bundlePath
47
+ * @returns {Promise<any>}
48
+ */
49
+ importBundleOnce(bundlePath) {
50
+ if (!bundlePath) {
51
+ return Promise.reject(new Error('Bundle path is required'));
52
+ }
53
+
54
+ if (this.bundleImportPromises.has(bundlePath)) {
55
+ return this.bundleImportPromises.get(bundlePath);
56
+ }
57
+
58
+ const importPromise = import(bundlePath).catch((error) => {
59
+ this.bundleImportPromises.delete(bundlePath);
60
+ throw error;
61
+ });
62
+
63
+ this.bundleImportPromises.set(bundlePath, importPromise);
64
+ return importPromise;
65
+ }
66
+
67
+ /**
68
+ * Validate Bundling V2 module contract.
69
+ * Requires named exports: SLICE_BUNDLE_META and registerAll.
70
+ * @param {any} bundleModule
71
+ * @param {string} [bundleName]
72
+ * @returns {{metadata: object, registerAll: Function}}
73
+ */
74
+ async validateBundleModule(bundleModule, bundleName = 'unknown') {
75
+ const metadata = bundleModule?.SLICE_BUNDLE_META;
76
+ const registerAll = bundleModule?.registerAll;
77
+
78
+ if (!metadata || typeof metadata !== 'object' || typeof registerAll !== 'function') {
79
+ throw new Error(
80
+ `Bundle "${bundleName}" missing Bundling V2 exports contract: requires SLICE_BUNDLE_META and registerAll`
81
+ );
82
+ }
83
+
84
+ return { metadata, registerAll };
85
+ }
86
+
42
87
  /**
43
88
  * 📦 Loads a bundle by name or category
44
89
  */
@@ -47,45 +92,46 @@ export default class Controller {
47
92
  return; // Already loaded
48
93
  }
49
94
 
50
- try {
51
- let bundleInfo = null;
95
+ let bundleInfo = null;
52
96
 
53
- if (bundleName === 'critical') {
54
- bundleInfo = this.bundleConfig?.bundles?.critical;
55
- } else {
56
- bundleInfo = this.bundleConfig?.bundles?.routes?.[bundleName];
57
- }
97
+ if (bundleName === 'critical') {
98
+ bundleInfo = this.bundleConfig?.bundles?.critical;
99
+ } else {
100
+ bundleInfo = this.bundleConfig?.bundles?.routes?.[bundleName];
101
+ }
58
102
 
59
- if (!bundleInfo && this.bundleConfig?.bundles?.routes && bundleName !== 'critical') {
60
- const normalizedName = bundleName?.toLowerCase();
61
- const matchedKey = Object.keys(this.bundleConfig.bundles.routes).find(
62
- (key) => key.toLowerCase() === normalizedName
63
- );
64
- if (matchedKey) {
65
- bundleInfo = this.bundleConfig.bundles.routes[matchedKey];
66
- }
103
+ if (!bundleInfo && this.bundleConfig?.bundles?.routes && bundleName !== 'critical') {
104
+ const normalizedName = bundleName?.toLowerCase();
105
+ const matchedKey = Object.keys(this.bundleConfig.bundles.routes).find(
106
+ (key) => key.toLowerCase() === normalizedName
107
+ );
108
+ if (matchedKey) {
109
+ bundleInfo = this.bundleConfig.bundles.routes[matchedKey];
67
110
  }
111
+ }
68
112
 
69
- if (!bundleInfo) {
70
- console.warn(`Bundle ${bundleName} not found in configuration`);
71
- return;
72
- }
113
+ if (!bundleInfo) {
114
+ console.warn(`Bundle ${bundleName} not found in configuration`);
115
+ return;
116
+ }
73
117
 
74
- const bundlePath = `/bundles/${bundleInfo.file}`;
118
+ const bundlePath = `/bundles/${bundleInfo.file}`;
75
119
 
76
- // Dynamic import of the bundle
77
- const bundleModule = await import(bundlePath);
120
+ const bundleModule = await this.importBundleOnce(bundlePath);
121
+ const { metadata, registerAll } = await this.validateBundleModule(bundleModule, bundleName);
78
122
 
79
- // Manually register components from the imported bundle
80
- if (bundleModule.SLICE_BUNDLE) {
81
- await this.registerBundle(bundleModule.SLICE_BUNDLE);
82
- }
123
+ await registerAll(this, slice.stylesManager);
83
124
 
84
- this.loadedBundles.add(bundleName);
85
- } catch (error) {
86
- console.warn(`Failed to load bundle ${bundleName}:`, error);
125
+ this.loadedBundles.add(bundleName);
126
+ const loadedBundleKey = metadata.bundleKey;
127
+ if (loadedBundleKey && loadedBundleKey !== bundleName) {
128
+ this.loadedBundles.add(loadedBundleKey);
129
+ }
130
+
131
+ if (metadata.type === 'critical' || bundleName === 'critical') {
132
+ this.criticalBundleLoaded = true;
133
+ }
87
134
  }
88
- }
89
135
 
90
136
  /**
91
137
  * 📦 Registers a bundle's components (called automatically by bundle files)
@@ -419,34 +419,35 @@ export default class Router {
419
419
  slice.loading.start();
420
420
  }
421
421
 
422
- if (existingComponent) {
423
- targetElement.innerHTML = '';
424
- if (existingComponent.update) {
425
- existingComponent.props = { ...existingComponent.props, ...params };
426
- await existingComponent.update();
427
- }
428
- targetElement.appendChild(existingComponent);
429
- await this.renderRoutesInComponent(existingComponent);
430
- } else {
431
- const component = await slice.build(componentName, {
432
- params,
433
- sliceId: sliceId,
434
- });
435
-
436
- targetElement.innerHTML = '';
437
- targetElement.appendChild(component);
422
+ try {
423
+ if (existingComponent) {
424
+ targetElement.innerHTML = '';
425
+ if (existingComponent.update) {
426
+ existingComponent.props = { ...existingComponent.props, ...params };
427
+ await existingComponent.update();
428
+ }
429
+ targetElement.appendChild(existingComponent);
430
+ await this.renderRoutesInComponent(existingComponent);
431
+ } else {
432
+ const component = await slice.build(componentName, {
433
+ params,
434
+ sliceId: sliceId,
435
+ });
438
436
 
439
- await this.renderRoutesInComponent(component);
440
- }
437
+ targetElement.innerHTML = '';
438
+ targetElement.appendChild(component);
441
439
 
442
- // Invalidar caché después de cambios importantes en el DOM
443
- this.invalidateCache();
440
+ await this.renderRoutesInComponent(component);
441
+ }
444
442
 
445
- if (slice.loading) {
446
- slice.loading.stop();
443
+ // Invalidar caché después de cambios importantes en el DOM
444
+ this.invalidateCache();
445
+ slice.router.activeRoute = route;
446
+ } finally {
447
+ if (slice.loading) {
448
+ slice.loading.stop();
449
+ }
447
450
  }
448
-
449
- slice.router.activeRoute = route;
450
451
  }
451
452
 
452
453
  /**
@@ -10,12 +10,22 @@ export default class StylesManager {
10
10
  /**
11
11
  * Load global styles and initialize ThemeManager if enabled.
12
12
  * @returns {Promise<void>}
13
- */
13
+ */
14
14
  async init() {
15
- for (let i = 0; i < slice.stylesConfig.requestedStyles.length; i++) {
16
- const styles = await slice.controller.fetchText(slice.stylesConfig.requestedStyles[i], 'styles');
17
- this.componentStyles.innerText += styles;
18
- slice.logger.logInfo('StylesManager', `${slice.stylesConfig.requestedStyles[i]} styles loaded`);
15
+ const requestedStyles = Array.isArray(slice.stylesConfig.requestedStyles)
16
+ ? slice.stylesConfig.requestedStyles
17
+ : [];
18
+
19
+ const styleResults = await Promise.all(
20
+ requestedStyles.map(async (styleName) => {
21
+ const styles = await slice.controller.fetchText(styleName, 'styles');
22
+ return { styleName, styles };
23
+ })
24
+ );
25
+
26
+ for (const { styleName, styles } of styleResults) {
27
+ this.appendComponentStyles(styles);
28
+ slice.logger.logInfo('StylesManager', `${styleName} styles loaded`);
19
29
  }
20
30
 
21
31
  if (slice.themeConfig.enabled) {
package/Slice/Slice.js CHANGED
@@ -269,28 +269,17 @@ async function init() {
269
269
  const envMode = envResult?.mode ?? null;
270
270
  const bundleConfigJson = configResult;
271
271
 
272
- // 3. Determine canonical mode: env endpoint takes precedence, then bundle config
273
- let resolvedMode;
274
- if (envMode) {
275
- resolvedMode = envMode;
276
- } else if (bundleConfigJson?.production) {
277
- resolvedMode = 'production';
278
- } else {
279
- resolvedMode = 'development';
280
- }
281
-
282
- // Pre-fetch critical bundle to warm the browser HTTP cache while the framework
283
- // bundle is downloading and executing. fetch() downloads bytes without evaluating
284
- // the module, so the auto-registration block runs safely later when window.slice
285
- // already exists. Errors are silently ignored — import() will retry if needed.
286
- const criticalBundleUrl = (resolvedMode === 'production' && bundleConfigJson?.bundles?.critical?.file)
287
- ? `/bundles/${bundleConfigJson.bundles.critical.file}`
288
- : null;
289
- if (criticalBundleUrl) {
290
- fetch(criticalBundleUrl).catch(() => {});
272
+ // 3. Determine canonical mode: env endpoint takes precedence, then bundle config
273
+ let resolvedMode;
274
+ if (envMode) {
275
+ resolvedMode = envMode;
276
+ } else if (bundleConfigJson?.production) {
277
+ resolvedMode = 'production';
278
+ } else {
279
+ resolvedMode = 'development';
291
280
  }
292
281
 
293
- // 4. Load framework classes.
282
+ // 4. Load framework classes.
294
283
  // In production the bundler generates slice-bundle.framework.js which
295
284
  // sets window.SLICE_FRAMEWORK_CLASSES. In dev mode always use individual
296
285
  // imports so the live /Slice/ source is served directly without bundles.
@@ -326,53 +315,60 @@ async function init() {
326
315
  window.slice = new Slice(sliceConfig, frameworkClasses);
327
316
  window.slice._mode = resolvedMode;
328
317
 
318
+ const createBundlingInitError = (step, error) => {
319
+ const detail = error instanceof Error ? error.message : String(error);
320
+ return new Error(`Bundling V2 initialization failed (${step}): ${detail}`, { cause: error });
321
+ };
322
+
329
323
  // Initialize bundles before building components.
330
324
  // Only in production — dev mode loads each component individually from source.
331
325
  // bundleConfigJson was already fetched above (step 2); reuse it.
332
- try {
333
- if (resolvedMode === 'production' && bundleConfigJson) {
334
- window.slice.controller.bundleConfig = bundleConfigJson;
335
- }
326
+ if (resolvedMode === 'production' && bundleConfigJson) {
327
+ window.slice.controller.bundleConfig = bundleConfigJson;
328
+ }
336
329
 
337
- if (window.slice.controller.bundleConfig) {
338
- const config = window.slice.controller.bundleConfig;
339
-
340
- const criticalFile = config?.bundles?.critical?.file;
341
- if (criticalFile) {
342
- // Bundle auto-registers itself on import via its own registration block.
343
- // The block pushes its registerBundle() Promise to window.__slicePendingRegistrations
344
- // so we can await full chunk processing before continuing to build('Loading').
345
- // criticalBundleUrl was pre-fetched above — import() reuses the cached bytes.
346
- await import(criticalBundleUrl);
347
- if (window.__slicePendingRegistrations?.length) {
348
- await Promise.all(window.__slicePendingRegistrations);
349
- window.__slicePendingRegistrations = [];
350
- }
330
+ if (resolvedMode === 'production' && window.slice.controller.bundleConfig) {
331
+ const config = window.slice.controller.bundleConfig;
332
+ const criticalFile = config?.bundles?.critical?.file;
333
+ if (criticalFile) {
334
+ try {
335
+ await window.slice.controller.loadBundle('critical');
336
+ } catch (error) {
337
+ throw createBundlingInitError(`critical bundle "${criticalFile}"`, error);
351
338
  }
339
+ }
352
340
 
353
- const routeBundles = config?.routeBundles || {};
354
- const initialPath = window.location.pathname || '/';
355
- const bundlesForRoute = routeBundles[initialPath] || [];
356
-
357
- const loadRouteBundles = async () => {
358
- for (const bundleName of bundlesForRoute) {
359
- if (bundleName === 'critical') continue;
360
- const bundleInfo = config?.bundles?.routes?.[bundleName];
361
- if (!bundleInfo?.file) continue;
362
- // Bundle auto-registers itself on import.
363
- await import(`/bundles/${bundleInfo.file}`);
364
- }
365
- };
366
-
367
- if (typeof requestIdleCallback === 'function') {
368
- requestIdleCallback(() => loadRouteBundles());
369
- } else {
370
- setTimeout(() => loadRouteBundles(), 0);
371
- }
372
- }
373
- } catch (error) {
374
- console.log('📄 Using individual component loading (no bundles found)');
375
- }
341
+ const routeBundles = config?.routeBundles || {};
342
+ const initialPath = window.location.pathname || '/';
343
+ const bundlesForRoute = routeBundles[initialPath] || [];
344
+
345
+ const loadRouteBundles = async () => {
346
+ for (const bundleName of bundlesForRoute) {
347
+ if (bundleName === 'critical') continue;
348
+ const bundleInfo = config?.bundles?.routes?.[bundleName];
349
+ if (!bundleInfo?.file) continue;
350
+ await window.slice.controller.loadBundle(bundleName);
351
+ }
352
+ };
353
+
354
+ const preloadRouteBundles = () => {
355
+ loadRouteBundles().catch((error) => {
356
+ const bundlingError = createBundlingInitError(
357
+ `idle route preload "${initialPath}"`,
358
+ error
359
+ );
360
+ queueMicrotask(() => {
361
+ throw bundlingError;
362
+ });
363
+ });
364
+ };
365
+
366
+ if (typeof requestIdleCallback === 'function') {
367
+ requestIdleCallback(() => preloadRouteBundles());
368
+ } else {
369
+ setTimeout(() => preloadRouteBundles(), 0);
370
+ }
371
+ }
376
372
 
377
373
  slice.paths.structuralComponentFolderPath = '/Slice/Components/Structural';
378
374
 
@@ -466,7 +462,8 @@ async function init() {
466
462
  }
467
463
  }
468
464
 
469
- await window.slice.stylesManager.init();
465
+ const stylesInitPromise = window.slice.stylesManager.init();
466
+ const routesModulePromise = import(slice.paths.routesFile);
470
467
 
471
468
  if (sliceConfig.events?.ui?.shortcut || sliceConfig.context?.ui?.shortcut) {
472
469
  const normalize = (value) => (typeof value === 'string' ? value.toLowerCase() : '');
@@ -496,7 +493,7 @@ async function init() {
496
493
  });
497
494
  }
498
495
 
499
- const routesModule = await import(slice.paths.routesFile);
496
+ const [, routesModule] = await Promise.all([stylesInitPromise, routesModulePromise]);
500
497
  const routes = routesModule.default;
501
498
  const RouterModule = window.slice.frameworkClasses?.Router
502
499
  || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Router/Router.js`);
@@ -0,0 +1,268 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { register } from 'node:module';
7
+ import { pathToFileURL } from 'node:url';
8
+
9
+ test('validateBundleModule rejects module missing Bundling V2 exports contract', async () => {
10
+ const tempDir = await mkdtemp(path.join(tmpdir(), 'slice-controller-loader-'));
11
+ const loaderPath = path.join(tempDir, 'components-alias-loader.mjs');
12
+ await writeFile(
13
+ loaderPath,
14
+ `export async function resolve(specifier, context, nextResolve) {
15
+ if (specifier === '/Components/components.js') {
16
+ return {
17
+ shortCircuit: true,
18
+ url: 'data:text/javascript,export default {};',
19
+ };
20
+ }
21
+ return nextResolve(specifier, context);
22
+ }
23
+ `,
24
+ 'utf8'
25
+ );
26
+ register(pathToFileURL(loaderPath).href);
27
+
28
+ const controllerModuleUrl = new URL('../Components/Structural/Controller/Controller.js', import.meta.url).href;
29
+ const { default: Controller } = await import(controllerModuleUrl);
30
+ const controller = new Controller();
31
+ const originalWindow = globalThis.window;
32
+
33
+ try {
34
+ globalThis.window = {
35
+ __slicePendingRegistrations: [],
36
+ };
37
+
38
+ assert.equal(
39
+ typeof controller.validateBundleModule,
40
+ 'function',
41
+ 'Controller must expose validateBundleModule for Bundling V2 runtime contract checks'
42
+ );
43
+
44
+ if (typeof controller.validateBundleModule === 'function') {
45
+ await assert.rejects(
46
+ () => Promise.resolve(controller.validateBundleModule({ default: {} }, 'critical.js')),
47
+ /missing bundling v2 exports contract/i
48
+ );
49
+ }
50
+ } finally {
51
+ globalThis.window = originalWindow;
52
+ await rm(tempDir, { recursive: true, force: true });
53
+ }
54
+ });
55
+
56
+ test('loadBundle marks requested bundle key and metadata bundleKey as loaded', async () => {
57
+ const tempDir = await mkdtemp(path.join(tmpdir(), 'slice-controller-loader-'));
58
+ const loaderPath = path.join(tempDir, 'components-alias-loader.mjs');
59
+ await writeFile(
60
+ loaderPath,
61
+ `export async function resolve(specifier, context, nextResolve) {
62
+ if (specifier === '/Components/components.js') {
63
+ return {
64
+ shortCircuit: true,
65
+ url: 'data:text/javascript,export default {};',
66
+ };
67
+ }
68
+ return nextResolve(specifier, context);
69
+ }
70
+ `,
71
+ 'utf8'
72
+ );
73
+ register(pathToFileURL(loaderPath).href);
74
+
75
+ const controllerModuleUrl = new URL('../Components/Structural/Controller/Controller.js', import.meta.url).href;
76
+ const { default: Controller } = await import(controllerModuleUrl);
77
+ const controller = new Controller();
78
+ const originalSlice = globalThis.slice;
79
+
80
+ controller.bundleConfig = {
81
+ bundles: {
82
+ routes: {
83
+ dashboard: {
84
+ file: 'dashboard.js',
85
+ },
86
+ },
87
+ },
88
+ };
89
+
90
+ controller.importBundleOnce = async () => ({});
91
+ controller.validateBundleModule = async () => ({
92
+ metadata: {
93
+ bundleKey: 'routes.dashboard.v2',
94
+ type: 'route',
95
+ },
96
+ registerAll: async () => {},
97
+ });
98
+
99
+ try {
100
+ globalThis.slice = {
101
+ stylesManager: {},
102
+ };
103
+
104
+ await controller.loadBundle('dashboard');
105
+
106
+ assert.equal(controller.loadedBundles.has('dashboard'), true);
107
+ assert.equal(controller.loadedBundles.has('routes.dashboard.v2'), true);
108
+ assert.equal(controller.criticalBundleLoaded, false);
109
+ } finally {
110
+ globalThis.slice = originalSlice;
111
+ await rm(tempDir, { recursive: true, force: true });
112
+ }
113
+ });
114
+
115
+ test('Slice init fails fast with contextual error on invalid Bundling V2 contract path', async () => {
116
+ const tempDir = await mkdtemp(path.join(tmpdir(), 'slice-init-loader-'));
117
+ const loaderPath = path.join(tempDir, 'components-alias-loader.mjs');
118
+ await writeFile(
119
+ loaderPath,
120
+ `export async function resolve(specifier, context, nextResolve) {
121
+ if (specifier === '/Components/components.js') {
122
+ return {
123
+ shortCircuit: true,
124
+ url: 'data:text/javascript,export default {};',
125
+ };
126
+ }
127
+ return nextResolve(specifier, context);
128
+ }
129
+ `,
130
+ 'utf8'
131
+ );
132
+ register(pathToFileURL(loaderPath).href);
133
+
134
+ const originalWindow = globalThis.window;
135
+ const originalDocument = globalThis.document;
136
+ const originalFetch = globalThis.fetch;
137
+ const originalAlert = globalThis.alert;
138
+ const originalSliceDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'slice');
139
+ const originalConsoleLog = console.log;
140
+ const originalConsoleWarn = console.warn;
141
+ const controllerModuleUrl = new URL('../Components/Structural/Controller/Controller.js', import.meta.url).href;
142
+ const { default: Controller } = await import(controllerModuleUrl);
143
+ const originalLoadBundle = Controller.prototype.loadBundle;
144
+
145
+ const loggedMessages = [];
146
+
147
+ try {
148
+ globalThis.window = {
149
+ location: {
150
+ pathname: '/dashboard',
151
+ origin: 'http://localhost',
152
+ },
153
+ };
154
+
155
+ Object.defineProperty(globalThis, 'slice', {
156
+ configurable: true,
157
+ get() {
158
+ return globalThis.window?.slice;
159
+ },
160
+ set(value) {
161
+ if (!globalThis.window) {
162
+ globalThis.window = {};
163
+ }
164
+ globalThis.window.slice = value;
165
+ },
166
+ });
167
+
168
+ Controller.prototype.loadBundle = async () => {
169
+ throw new Error(
170
+ 'Bundle "critical" missing Bundling V2 exports contract: requires SLICE_BUNDLE_META and registerAll'
171
+ );
172
+ };
173
+
174
+ globalThis.document = {
175
+ head: {
176
+ appendChild() {},
177
+ },
178
+ body: {
179
+ appendChild() {},
180
+ },
181
+ createElement() {
182
+ return {
183
+ appendChild() {},
184
+ innerHTML: '',
185
+ id: '',
186
+ };
187
+ },
188
+ createTextNode() {
189
+ return {};
190
+ },
191
+ addEventListener() {},
192
+ };
193
+
194
+ globalThis.alert = () => {};
195
+ console.log = (...args) => {
196
+ loggedMessages.push(args.map(String).join(' '));
197
+ };
198
+ console.warn = () => {};
199
+
200
+ globalThis.fetch = async (url) => {
201
+ if (url === '/sliceConfig.json') {
202
+ return {
203
+ ok: true,
204
+ json: async () => ({
205
+ paths: {
206
+ routesFile: '/routes.js',
207
+ components: {},
208
+ },
209
+ themeManager: { enabled: false },
210
+ stylesManager: { requestedStyles: [] },
211
+ logger: { enabled: false },
212
+ debugger: { enabled: false },
213
+ loading: { enabled: false },
214
+ events: { enabled: false },
215
+ context: { enabled: false },
216
+ }),
217
+ };
218
+ }
219
+
220
+ if (url === '/slice-env.json') {
221
+ return {
222
+ ok: true,
223
+ json: async () => ({ mode: 'production' }),
224
+ };
225
+ }
226
+
227
+ if (url === '/bundles/bundle.config.json') {
228
+ return {
229
+ ok: true,
230
+ json: async () => ({
231
+ production: true,
232
+ bundles: {
233
+ critical: { file: 'critical.js' },
234
+ routes: {},
235
+ },
236
+ routeBundles: {},
237
+ }),
238
+ };
239
+ }
240
+
241
+ throw new Error(`Unexpected fetch URL: ${url}`);
242
+ };
243
+
244
+ const sliceModuleUrl = new URL(`../Slice.js?fail-fast-test=${Date.now()}`, import.meta.url).href;
245
+
246
+ await assert.rejects(() => import(sliceModuleUrl), /Bundling V2 initialization failed/i);
247
+
248
+ assert.equal(
249
+ loggedMessages.some((message) => message.includes('Using individual component loading (no bundles found)')),
250
+ false,
251
+ 'init must not log fallback bundle message on Bundling V2 contract failure'
252
+ );
253
+ } finally {
254
+ Controller.prototype.loadBundle = originalLoadBundle;
255
+ globalThis.window = originalWindow;
256
+ globalThis.document = originalDocument;
257
+ globalThis.fetch = originalFetch;
258
+ globalThis.alert = originalAlert;
259
+ if (originalSliceDescriptor) {
260
+ Object.defineProperty(globalThis, 'slice', originalSliceDescriptor);
261
+ } else {
262
+ delete globalThis.slice;
263
+ }
264
+ console.log = originalConsoleLog;
265
+ console.warn = originalConsoleWarn;
266
+ await rm(tempDir, { recursive: true, force: true });
267
+ }
268
+ });