slicejs-web-framework 2.4.6 → 2.4.8

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.
@@ -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,33 @@ 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
+
42
67
  /**
43
68
  * 📦 Loads a bundle by name or category
44
69
  */
@@ -73,19 +98,12 @@ export default class Controller {
73
98
 
74
99
  const bundlePath = `/bundles/${bundleInfo.file}`;
75
100
 
76
- const integrityOk = await this.verifyBundleIntegrity(bundlePath, bundleInfo.integrity);
77
- if (!integrityOk) {
78
- console.warn(`❌ Integrity check failed for bundle ${bundleName}`);
79
- return;
80
- }
81
-
82
- // Dynamic import of the bundle
83
- const bundleModule = await import(bundlePath);
101
+ await this.importBundleOnce(bundlePath);
84
102
 
85
- // Manually register components from the imported bundle
86
- if (bundleModule.SLICE_BUNDLE) {
87
- await this.registerBundle(bundleModule.SLICE_BUNDLE);
88
- }
103
+ if (window.__slicePendingRegistrations?.length) {
104
+ await Promise.all(window.__slicePendingRegistrations);
105
+ window.__slicePendingRegistrations = [];
106
+ }
89
107
 
90
108
  this.loadedBundles.add(bundleName);
91
109
  } catch (error) {
@@ -93,47 +111,6 @@ export default class Controller {
93
111
  }
94
112
  }
95
113
 
96
- /**
97
- * Verifies bundle integrity by comparing sha256 hash.
98
- * @param {string} bundlePath
99
- * @param {string|null} expectedIntegrity
100
- * @returns {Promise<boolean>}
101
- */
102
- async verifyBundleIntegrity(bundlePath, expectedIntegrity) {
103
- if (!expectedIntegrity) {
104
- return true;
105
- }
106
-
107
- try {
108
- const response = await fetch(bundlePath, { cache: 'no-store' });
109
- if (!response.ok) {
110
- console.warn(`Failed to fetch bundle for integrity check: ${response.status}`);
111
- return false;
112
- }
113
-
114
- const content = await response.text();
115
- const actualIntegrity = await this.hashSha256(content);
116
- return actualIntegrity === expectedIntegrity;
117
- } catch (error) {
118
- console.warn(`Integrity check error for ${bundlePath}:`, error);
119
- return false;
120
- }
121
- }
122
-
123
- /**
124
- * Calculates sha256 digest for a string.
125
- * @param {string} content
126
- * @returns {Promise<string>}
127
- */
128
- async hashSha256(content) {
129
- const encoder = new TextEncoder();
130
- const data = encoder.encode(content);
131
- const hashBuffer = await crypto.subtle.digest('SHA-256', data);
132
- const hashArray = Array.from(new Uint8Array(hashBuffer));
133
- const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
134
- return `sha256:${hashHex}`;
135
- }
136
-
137
114
  /**
138
115
  * 📦 Registers a bundle's components (called automatically by bundle files)
139
116
  */
@@ -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,27 +269,20 @@ 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
 
282
+ const criticalBundleUrl = (resolvedMode === 'production' && bundleConfigJson?.bundles?.critical?.file)
283
+ ? `/bundles/${bundleConfigJson.bundles.critical.file}`
284
+ : null;
285
+
293
286
  // 4. Load framework classes.
294
287
  // In production the bundler generates slice-bundle.framework.js which
295
288
  // sets window.SLICE_FRAMEWORK_CLASSES. In dev mode always use individual
@@ -337,18 +330,17 @@ async function init() {
337
330
  if (window.slice.controller.bundleConfig) {
338
331
  const config = window.slice.controller.bundleConfig;
339
332
 
340
- const criticalFile = config?.bundles?.critical?.file;
341
- if (criticalFile) {
333
+ const criticalFile = config?.bundles?.critical?.file;
334
+ if (criticalFile && criticalBundleUrl) {
342
335
  // Bundle auto-registers itself on import via its own registration block.
343
336
  // The block pushes its registerBundle() Promise to window.__slicePendingRegistrations
344
337
  // 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
- }
351
- }
338
+ await window.slice.controller.importBundleOnce(criticalBundleUrl);
339
+ if (window.__slicePendingRegistrations?.length) {
340
+ await Promise.all(window.__slicePendingRegistrations);
341
+ window.__slicePendingRegistrations = [];
342
+ }
343
+ }
352
344
 
353
345
  const routeBundles = config?.routeBundles || {};
354
346
  const initialPath = window.location.pathname || '/';
@@ -360,9 +352,9 @@ async function init() {
360
352
  const bundleInfo = config?.bundles?.routes?.[bundleName];
361
353
  if (!bundleInfo?.file) continue;
362
354
  // Bundle auto-registers itself on import.
363
- await import(`/bundles/${bundleInfo.file}`);
355
+ await window.slice.controller.importBundleOnce(`/bundles/${bundleInfo.file}`);
364
356
  }
365
- };
357
+ };
366
358
 
367
359
  if (typeof requestIdleCallback === 'function') {
368
360
  requestIdleCallback(() => loadRouteBundles());
@@ -466,7 +458,8 @@ async function init() {
466
458
  }
467
459
  }
468
460
 
469
- await window.slice.stylesManager.init();
461
+ const stylesInitPromise = window.slice.stylesManager.init();
462
+ const routesModulePromise = import(slice.paths.routesFile);
470
463
 
471
464
  if (sliceConfig.events?.ui?.shortcut || sliceConfig.context?.ui?.shortcut) {
472
465
  const normalize = (value) => (typeof value === 'string' ? value.toLowerCase() : '');
@@ -496,7 +489,7 @@ async function init() {
496
489
  });
497
490
  }
498
491
 
499
- const routesModule = await import(slice.paths.routesFile);
492
+ const [, routesModule] = await Promise.all([stylesInitPromise, routesModulePromise]);
500
493
  const routes = routesModule.default;
501
494
  const RouterModule = window.slice.frameworkClasses?.Router
502
495
  || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Router/Router.js`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slicejs-web-framework",
3
- "version": "2.4.6",
3
+ "version": "2.4.8",
4
4
  "description": "",
5
5
  "engines": {
6
6
  "node": ">=20"