slicejs-web-framework 4.1.1 → 4.2.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.
package/Slice/Slice.js CHANGED
@@ -1,711 +1,826 @@
1
-
2
- /**
3
- * Main Slice.js runtime.
4
- */
5
- export default class Slice {
6
- /**
7
- * @param {Object} sliceConfig
8
- */
9
- constructor(sliceConfig, frameworkClasses = null) {
10
- this.frameworkClasses = frameworkClasses;
11
- const ControllerClass = frameworkClasses?.Controller;
12
- const StylesManagerClass = frameworkClasses?.StylesManager;
13
-
14
- this.controller = ControllerClass ? new ControllerClass() : null;
15
- this.stylesManager = StylesManagerClass ? new StylesManagerClass() : null;
16
- this.paths = sliceConfig.paths;
17
- this.themeConfig = sliceConfig.themeManager;
18
- this.stylesConfig = sliceConfig.stylesManager;
19
- this.loggerConfig = sliceConfig.logger;
20
- this.debuggerConfig = sliceConfig.debugger;
21
- this.loadingConfig = sliceConfig.loading;
22
- this.eventsConfig = sliceConfig.events;
23
- this.routerConfig = sliceConfig.router;
24
-
25
- // Default to production until init() resolves the actual mode.
26
- // Safe to call isProduction() before init() completes.
27
- this._mode = 'production';
28
- this._publicEnv = {};
29
-
30
- // 📦 Bundle system is initialized automatically via import in index.js
31
- }
32
-
33
- /**
34
- * Dynamically import a module and return its default export.
35
- * @param {string} module
36
- * @returns {Promise<any>}
37
- */
38
- async getClass(module) {
39
- try {
40
- const { default: myClass } = await import(module);
41
- return await myClass;
42
- } catch (error) {
43
- this.logger.error('Slice', `Error loading class ${module}`, error);
44
- throw error;
45
- }
46
- }
47
-
48
- /**
49
- * Returns true when running in production mode.
50
- * Reliable after init() has completed.
51
- * @returns {boolean}
52
- */
53
- isProduction() {
54
- return this._mode === 'production';
55
- }
56
-
57
- setPublicEnv(envPayload = {}) {
58
- const normalized = {};
59
-
60
- for (const [key, value] of Object.entries(envPayload || {})) {
61
- if (!key.startsWith('SLICE_PUBLIC_')) continue;
62
- normalized[key] = String(value ?? '');
63
- }
64
-
65
- this._publicEnv = normalized;
66
- }
67
-
68
- getEnv(name, fallbackValue = undefined) {
69
- if (!name || typeof name !== 'string') {
70
- return fallbackValue;
71
- }
72
-
73
- if (Object.prototype.hasOwnProperty.call(this._publicEnv, name)) {
74
- return this._publicEnv[name];
75
- }
76
-
77
- return fallbackValue;
78
- }
79
-
80
- getPublicEnv() {
81
- return { ...this._publicEnv };
82
- }
83
-
84
- /**
85
- * Typed accessors over the public env, so callers stop re-parsing strings.
86
- * slice.env.get('SLICE_PUBLIC_API_URL', '')
87
- * slice.env.bool('SLICE_PUBLIC_AUTH_ENABLED') // '1'|'true'|'yes'|'on' → true
88
- * slice.env.int('SLICE_PUBLIC_TIMEOUT', 5000)
89
- * slice.env.list('SLICE_PUBLIC_MODELS') // 'a, b' → ['a','b']
90
- * slice.env.has('X') / slice.env.all()
91
- * @returns {{ get: Function, has: Function, all: Function, bool: Function, int: Function, list: Function }}
92
- */
93
- get env() {
94
- const read = (name) =>
95
- Object.prototype.hasOwnProperty.call(this._publicEnv, name) ? this._publicEnv[name] : undefined;
96
- const present = (v) => v !== undefined && String(v).trim() !== '';
97
-
98
- return {
99
- get: (name, fallback = undefined) => this.getEnv(name, fallback),
100
- has: (name) => Object.prototype.hasOwnProperty.call(this._publicEnv, name),
101
- all: () => this.getPublicEnv(),
102
- bool: (name, fallback = false) => {
103
- const v = read(name);
104
- return present(v) ? ['1', 'true', 'yes', 'on'].includes(String(v).trim().toLowerCase()) : fallback;
105
- },
106
- int: (name, fallback = 0) => {
107
- const v = read(name);
108
- const n = parseInt(v, 10);
109
- return Number.isNaN(n) ? fallback : n;
110
- },
111
- list: (name, fallback = []) => {
112
- const v = read(name);
113
- if (!present(v)) return fallback;
114
- return String(v)
115
- .split(',')
116
- .map((s) => s.trim())
117
- .filter(Boolean);
118
- },
119
- };
120
- }
121
-
122
- /**
123
- * Get a component instance by sliceId.
124
- * @param {string} componentSliceId
125
- * @returns {HTMLElement|undefined}
126
- */
127
- getComponent(componentSliceId) {
128
- return this.controller.activeComponents.get(componentSliceId);
129
- }
130
-
131
- /**
132
- * Apply props to a component. The controller auto-detects build vs refresh:
133
- * during construction (not yet registered) it applies defaults + required
134
- * validation; once registered, it refreshes in place (no defaults, no required,
135
- * respects diff-guards). Same call works for both.
136
- * @param {HTMLElement} component
137
- * @param {Object} props
138
- * @returns {void}
139
- */
140
- setComponentProps(component, props) {
141
- return this.controller.setComponentProps(component, props);
142
- }
143
-
144
- /**
145
- * Whether `component` is still the live registered instance for its sliceId
146
- * (false after it has been destroyed, or replaced by a rebuild).
147
- * @param {HTMLElement} component
148
- * @returns {boolean}
149
- */
150
- isComponentAlive(component) {
151
- return !!component && this.controller.activeComponents.get(component.sliceId) === component;
152
- }
153
-
154
- /**
155
- * Build a component instance and run init.
156
- *
157
- * Pass `{ singleton: true }` to get-or-create one shared instance keyed by
158
- * `sliceId` (defaults to `componentName`). Concurrent singleton builds of the
159
- * same id share a single in-flight build, so they never race on a duplicate
160
- * sliceId. Singletons are only supported for Service components — for app-wide
161
- * UI build a Provider Service that manages the Visual (see ToastProvider /
162
- * ToolTipProvider), because a DOM node can only live in one place.
163
- *
164
- * Note: `props` only apply on the first (creating) call; later calls return
165
- * the existing instance and ignore them.
166
- *
167
- * @param {string} componentName
168
- * @param {Object} [props]
169
- * @param {boolean} [props.singleton] Reuse a single instance per sliceId.
170
- * @returns {Promise<HTMLElement|Object|null>}
171
- */
172
- async build(componentName, props = {}) {
173
- if (!props || props.singleton !== true) {
174
- return this._build(componentName, props);
175
- }
176
-
177
- const { singleton, ...rest } = props;
178
- const sliceId = rest.sliceId || componentName;
179
-
180
- // Singletons are allowed for any category whose *type* is 'Service' — not
181
- // only the built-in 'Service' category. Custom categories declared in
182
- // sliceConfig with `"type": "Service"` (e.g. AppServices) are services too.
183
- const category = this.controller.componentCategories.get(componentName);
184
- const categoryType = slice.paths?.components?.[category]?.type;
185
- if (categoryType !== 'Service') {
186
- this.logger.logError(
187
- 'Slice',
188
- `singleton:true is only supported for Service-type components ('${componentName}' is in category '${category || 'unknown'}', type '${categoryType || 'unknown'}'). ` +
189
- `For app-wide UI build a Provider Service that manages the Visual (see ToastProvider/ToolTipProvider).`
190
- );
191
- return null;
192
- }
193
-
194
- return this.controller.getOrCreate(sliceId, () =>
195
- this._build(componentName, { ...rest, sliceId })
196
- );
197
- }
198
-
199
- /**
200
- * Internal build: load resources, construct, run init, register. Always
201
- * creates a new instance. Public `build` delegates here (and wraps it with
202
- * get-or-create when `singleton:true`).
203
- * @param {string} componentName
204
- * @param {Object} [props]
205
- * @returns {Promise<HTMLElement|Object|null>}
206
- */
207
- async _build(componentName, props = {}) {
208
- if (!componentName) {
209
- this.logger.error('Slice', 'Component name is required to build a component');
210
- return null;
211
- }
212
-
213
- if (typeof componentName !== 'string') {
214
- this.logger.error('Slice', 'Component name must be a string');
215
- return null;
216
- }
217
-
218
- // 📦 Try to load from bundles first
219
- const bundleName = this.controller.getBundleForComponent(componentName);
220
- if (bundleName && !this.controller.loadedBundles.has(bundleName)) {
221
- await this.controller.loadBundle(bundleName);
222
- }
223
-
224
- if (!this.controller.componentCategories.has(componentName) && !this.controller.classes.has(componentName)) {
225
- this.logger.error('Slice', `Component ${componentName} not found in components.js file`);
226
- return null;
227
- }
228
-
229
- let componentCategory = this.controller.componentCategories.get(componentName);
230
- if (!componentCategory && this.controller.classes.has(componentName)) {
231
- componentCategory = 'AppComponents';
232
- }
233
-
234
- // 📦 Check if component is already available from loaded bundles
235
- const isFromBundle = this.controller.isComponentFromBundle(componentName);
236
-
237
- if (componentCategory === 'Structural') {
238
- this.logger.error('Slice', `Component ${componentName} is a Structural component and cannot be built`);
239
- return null;
240
- }
241
-
242
- let isVisual = slice.paths.components[componentCategory]?.type === 'Visual';
243
- let modulePath = `${slice.paths.components[componentCategory].path}/${componentName}/${componentName}.js`;
244
- const isJsOnlyVisualComponent = isVisual && (componentName === 'MultiRoute' || componentName === 'Route');
245
-
246
- // Load template, class, and CSS concurrently if needed
247
- try {
248
- // 📦 Skip individual loading if component is available from bundles
249
- const loadTemplate =
250
- isFromBundle || !isVisual || isJsOnlyVisualComponent || this.controller.templates.has(componentName)
251
- ? Promise.resolve(null)
252
- : this.controller.fetchText(componentName, 'html', componentCategory);
253
-
254
- const loadClass =
255
- isFromBundle || this.controller.classes.has(componentName)
256
- ? Promise.resolve(null)
257
- : this.getClass(modulePath);
258
-
259
- const loadCSS =
260
- isFromBundle || !isVisual || isJsOnlyVisualComponent || this.controller.requestedStyles.has(componentName)
261
- ? Promise.resolve(null)
262
- : this.controller.fetchText(componentName, 'css', componentCategory);
263
-
264
- const [html, ComponentClass, css] = await Promise.all([loadTemplate, loadClass, loadCSS]);
265
-
266
- // 📦 If component is from bundle but not in cache, it should have been registered by registerBundle
267
- if (isFromBundle) {
268
- this.logger.logInfo('Slice', `📦 Using bundled component: ${componentName}`);
269
- }
270
-
271
- if (html || html === '') {
272
- const template = document.createElement('template');
273
- template.innerHTML = html;
274
- this.controller.templates.set(componentName, template);
275
- this.logger.logInfo('Slice', `Template ${componentName} loaded`);
276
- }
277
-
278
- if (ComponentClass) {
279
- this.controller.classes.set(componentName, ComponentClass);
280
- this.logger.logInfo('Slice', `Class ${componentName} loaded`);
281
- }
282
-
283
- if (css) {
284
- this.stylesManager.registerComponentStyles(componentName, css);
285
- this.logger.logInfo('Slice', `CSS ${componentName} loaded`);
286
- }
287
- } catch (error) {
288
- this.logger.logError('Slice', `Error loading resources for ${componentName}`, error);
289
- return null;
290
- }
291
-
292
- // Create instance
293
- try {
294
- let componentIds = {};
295
- if (props.id) componentIds.id = props.id;
296
- if (props.sliceId) componentIds.sliceId = props.sliceId;
297
-
298
- delete props.id;
299
- delete props.sliceId;
300
- // `singleton` is a build directive (handled in the public build()
301
- // wrapper). Strip it here too so it is consistently reserved and never
302
- // leaks into a component's props on the non-singleton path.
303
- delete props.singleton;
304
-
305
- const ComponentClass = this.controller.classes.get(componentName);
306
- this.logger.logInfo(
307
- 'Slice',
308
- `🔎 Build component: ${componentName} (classType: ${typeof ComponentClass}, isFunction: ${typeof ComponentClass === 'function'})`
309
- );
310
- const componentInstance = new ComponentClass(props);
311
-
312
- if (componentIds.id && isVisual) componentInstance.id = componentIds.id;
313
- if (componentIds.sliceId) componentInstance.sliceId = componentIds.sliceId;
314
-
315
- if (!this.controller.verifyComponentIds(componentInstance)) {
316
- this.logger.logError('Slice', `Error registering instance ${componentName} ${componentInstance.sliceId}`);
317
- return null;
318
- }
319
-
320
- // Register before init so the component is visible in activeComponents
321
- // during init(). This lets slice.router.navigate() work from inside
322
- // init() without the Router creating a duplicate instance (the Router
323
- // finds the existing component via getComponent). Calling update()
324
- // from init() is safe because installUpdate already wrapped it.
325
- this.controller.registerComponent(componentInstance);
326
-
327
- if (componentInstance.init) await componentInstance.init();
328
-
329
- if (slice.debuggerConfig.enabled && isVisual) {
330
- this.debugger.attachDebugMode(componentInstance);
331
- }
332
-
333
- if (isVisual) {
334
- this.controller.registerComponentsRecursively(componentInstance);
335
- }
336
-
337
- this.logger.logInfo('Slice', `Instance ${componentInstance.sliceId} created`);
338
- return componentInstance;
339
- } catch (error) {
340
- this.logger.logError('Slice', `Error creating instance ${componentName}`, error);
341
- return null;
342
- }
343
- }
344
-
345
- /**
346
- * Apply a theme by name.
347
- * @param {string} themeName
348
- * @returns {Promise<void>}
349
- */
350
- async setTheme(themeName) {
351
- await this.stylesManager.themeManager.applyTheme(themeName);
352
- }
353
-
354
- /**
355
- * Current theme name.
356
- * @returns {string|null}
357
- */
358
- get theme() {
359
- return this.stylesManager.themeManager.currentTheme;
360
- }
361
-
362
- /**
363
- * Attach HTML template to a component instance.
364
- * @param {HTMLElement} componentInstance
365
- * @returns {void}
366
- */
367
- attachTemplate(componentInstance) {
368
- this.controller.loadTemplateToComponent(componentInstance);
369
- }
370
- }
371
-
372
- async function loadConfig() {
373
- try {
374
- const response = await fetch('/sliceConfig.json');
375
- if (!response.ok) throw new Error('Error loading sliceConfig.json');
376
- const json = await response.json();
377
- return json;
378
- } catch (error) {
379
- console.error('Error loading config file:', error);
380
- return null;
381
- }
382
- }
383
-
384
- async function init() {
385
- const sliceConfig = await loadConfig();
386
- if (!sliceConfig) {
387
- console.error('%c\u26A0\uFE0F Error loading Slice configuration', 'color: red; font-size: 20px;');
388
- alert('Error loading Slice configuration');
389
- throw new Error('Slice initialization failed: unable to load sliceConfig.json');
390
- }
391
-
392
- // 1+2. Fetch mode endpoint and bundle config in parallel — both are independent.
393
- // In production, /slice-env.json returns 404 (catch is expected and normal).
394
- // bundleConfigJson.production serves as a mode fallback when env endpoint is absent.
395
- let frameworkClasses = null;
396
- const [envResult, configResult] = await Promise.all([
397
- fetch('/slice-env.json', { cache: 'no-store' })
398
- .then(r => r.ok ? r.json() : null)
399
- .catch(error => { console.error('[Slice.js] Error fetching /slice-env.json:', error); return null; }),
400
- fetch('/bundles/bundle.config.json', { cache: 'no-store' })
401
- .then(r => r.ok ? r.json() : null)
402
- .catch(error => { console.error('[Slice.js] Error fetching bundle.config.json:', error); return null; })
403
- ]);
404
- const envMode = envResult?.mode ?? null;
405
- const bundleConfigJson = configResult;
406
-
407
- // 3. Determine canonical mode: env endpoint takes precedence, then bundle config
408
- let resolvedMode;
409
- if (envMode) {
410
- resolvedMode = envMode;
411
- } else if (bundleConfigJson?.production) {
412
- resolvedMode = 'production';
413
- } else {
414
- resolvedMode = 'development';
415
- }
416
-
417
- // 4. Load framework classes.
418
- // In production the bundler generates slice-bundle.framework.js which
419
- // sets window.SLICE_FRAMEWORK_CLASSES. In dev mode always use individual
420
- // imports so the live /Slice/ source is served directly without bundles.
421
- if (resolvedMode === 'production' && bundleConfigJson?.bundles?.framework?.file) {
422
- try {
423
- await import(`/bundles/${bundleConfigJson.bundles.framework.file}`);
424
- if (window.SLICE_FRAMEWORK_CLASSES) {
425
- frameworkClasses = window.SLICE_FRAMEWORK_CLASSES;
426
- }
427
- } catch (e) {
428
- console.error('[Slice.js] framework bundle import failed, falling through to individual imports:', e);
429
- }
430
- }
431
-
432
- if (!frameworkClasses) {
433
- try {
434
- const imports = await Promise.all([
435
- import('./Components/Structural/Controller/Controller.js'),
436
- import('./Components/Structural/StylesManager/StylesManager.js')
437
- ]);
438
- frameworkClasses = {
439
- Controller: imports[0].default,
440
- StylesManager: imports[1].default
441
- };
442
- } catch (e) {
443
- console.error('[Slice.js] individual imports fallback failed:', e);
444
- throw e;
445
- }
446
- }
447
-
448
- // 5. Create Slice instance and set resolved mode
449
- window.slice = new Slice(sliceConfig, frameworkClasses);
450
- window.slice._mode = resolvedMode;
451
- window.slice.setPublicEnv(envResult?.env || {});
452
-
453
- slice.paths.structuralComponentFolderPath = '/Slice/Components/Structural';
454
-
455
- // Logger must be available before bundle loading — Controller internally calls
456
- // slice.logger.* methods during loadBundle/registerBundle.
457
- if (sliceConfig.logger.enabled) {
458
- const LoggerModule = window.slice.frameworkClasses?.Logger
459
- || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Logger/Logger.js`);
460
- window.slice.logger = new LoggerModule();
461
- } else {
462
- const noop = () => {};
463
- window.slice.logger = {
464
- error: noop, warn: noop, info: noop, debug: noop,
465
- logError: noop, logWarning: noop, logInfo: noop,
466
- };
467
- }
468
-
469
- const createBundlingInitError = (step, error) => {
470
- const detail = error instanceof Error ? error.message : String(error);
471
- return new Error(`Bundling V2 initialization failed (${step}): ${detail}`, { cause: error });
472
- };
473
-
474
- // Initialize bundles before building components.
475
- // Only in production — dev mode loads each component individually from source.
476
- // bundleConfigJson was already fetched above (step 2); reuse it.
477
- if (resolvedMode === 'production' && bundleConfigJson) {
478
- window.slice.controller.bundleConfig = bundleConfigJson;
479
- }
480
-
481
- if (resolvedMode === 'production' && window.slice.controller.bundleConfig) {
482
- const config = window.slice.controller.bundleConfig;
483
- if (!window.__SLICE_SHARED_DEPS__ || typeof window.__SLICE_SHARED_DEPS__ !== 'object') {
484
- window.__SLICE_SHARED_DEPS__ = {};
485
- }
486
- const criticalFile = config?.bundles?.critical?.file;
487
- if (criticalFile) {
488
- try {
489
- await window.slice.controller.loadBundle('critical');
490
- } catch (error) {
491
- throw createBundlingInitError(`critical bundle "${criticalFile}"`, error);
492
- }
493
- }
494
-
495
- const routeBundles = config?.routeBundles || {};
496
- const initialPath = window.location.pathname || '/';
497
- const bundlesForRoute = routeBundles[initialPath] || [];
498
-
499
- const loadRouteBundles = async () => {
500
- for (const bundleName of bundlesForRoute) {
501
- if (bundleName === 'critical') continue;
502
- const bundleInfo = config?.bundles?.routes?.[bundleName];
503
- if (!bundleInfo?.file) continue;
504
- await window.slice.controller.loadBundle(bundleName);
505
- }
506
- };
507
-
508
- const preloadRouteBundles = () => {
509
- loadRouteBundles().catch((error) => {
510
- window.slice?.logger?.error('Slice', `Idle route preload failed for "${initialPath}"`, error);
511
- });
512
- };
513
-
514
- const safePreload = () => {
515
- try {
516
- preloadRouteBundles();
517
- } catch (error) {
518
- window.slice?.logger?.error('Slice', 'Error in route preload callback', error);
519
- }
520
- };
521
-
522
- if (typeof requestIdleCallback === 'function') {
523
- requestIdleCallback(() => safePreload());
524
- } else {
525
- setTimeout(() => safePreload(), 0);
526
- }
527
- }
528
-
529
- if (sliceConfig.debugger.enabled) {
530
- const DebuggerModule = window.slice.frameworkClasses?.Debugger
531
- || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Debugger/Debugger.js`);
532
- window.slice.debugger = new DebuggerModule();
533
- await window.slice.debugger.enableDebugMode();
534
- document.body.appendChild(window.slice.debugger);
535
- }
536
-
537
- if (sliceConfig.events?.ui?.enabled) {
538
- const EventsDebuggerModule = window.slice.frameworkClasses?.EventManagerDebugger
539
- || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/EventManager/EventManagerDebugger.js`);
540
- window.slice.eventsDebugger = new EventsDebuggerModule();
541
- await window.slice.eventsDebugger.init();
542
- document.body.appendChild(window.slice.eventsDebugger);
543
- }
544
-
545
- if (sliceConfig.context?.ui?.enabled) {
546
- const ContextDebuggerModule = window.slice.frameworkClasses?.ContextManagerDebugger
547
- || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/ContextManager/ContextManagerDebugger.js`);
548
- window.slice.contextDebugger = new ContextDebuggerModule();
549
- await window.slice.contextDebugger.init();
550
- document.body.appendChild(window.slice.contextDebugger);
551
- }
552
-
553
- if (sliceConfig.debugger?.leakInspector?.enabled && !window.slice.isProduction()) {
554
- const LeakInspectorModule = window.slice.frameworkClasses?.LeakInspector
555
- || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/LeakInspector/LeakInspector.js`);
556
- window.slice.leakInspector = new LeakInspectorModule();
557
- await window.slice.leakInspector.init();
558
- document.body.appendChild(window.slice.leakInspector);
559
- }
560
-
561
- // ContextManager delivers setState → watch through EventManager, so context
562
- // reactivity requires events. We don't auto-enable events (in a production
563
- // build EventManager is only bundled when events.enabled was true, so it may
564
- // not have shipped) nor throw (a boot-time throw would take down the whole
565
- // app, inconsistent with how "EventManager disabled" is handled below). Log a
566
- // loud error and continue; `slice build` hard-fails on this same misconfig so
567
- // a broken bundle never ships.
568
- if (sliceConfig.context?.enabled && !sliceConfig.events?.enabled) {
569
- window.slice.logger.logError(
570
- 'Slice',
571
- 'context.enabled requires events.enabled — ContextManager reactivity (setState → watch/bind) is delivered through EventManager. With events disabled, watchers will not fire. Set "events": { "enabled": true } in sliceConfig.json.'
572
- );
573
- }
574
-
575
- if (sliceConfig.events?.enabled) {
576
- const EventManagerModule = window.slice.frameworkClasses?.EventManager
577
- || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/EventManager/EventManager.js`);
578
- window.slice.events = new EventManagerModule();
579
- if (typeof window.slice.events.init === 'function') {
580
- await window.slice.events.init();
581
- }
582
- } else {
583
- window.slice.events = {
584
- subscribe: () => null,
585
- subscribeOnce: () => null,
586
- unsubscribe: () => false,
587
- emit: () => {},
588
- register: () => window.slice.events,
589
- loadGraph: () => window.slice.events,
590
- isDeclared: () => false,
591
- bind: () => ({
592
- subscribe: () => null,
593
- subscribeOnce: () => null,
594
- emit: () => {},
595
- register: () => window.slice.events,
596
- }),
597
- cleanupComponent: () => 0,
598
- hasSubscribers: () => false,
599
- subscriberCount: () => 0,
600
- clear: () => {},
601
- };
602
- window.slice.logger.logError('Slice', 'EventManager disabled');
603
- }
604
-
605
- if (sliceConfig.context?.enabled) {
606
- const ContextManagerModule = window.slice.frameworkClasses?.ContextManager
607
- || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/ContextManager/ContextManager.js`);
608
- window.slice.context = new ContextManagerModule();
609
- if (typeof window.slice.context.init === 'function') {
610
- await window.slice.context.init();
611
- }
612
- } else {
613
- window.slice.context = {
614
- create: () => false,
615
- getState: () => null,
616
- setState: () => {},
617
- watch: () => null,
618
- has: () => false,
619
- destroy: () => false,
620
- list: () => [],
621
- };
622
- window.slice.logger.logError('Slice', 'ContextManager disabled');
623
- }
624
-
625
- if (sliceConfig.logger?.ui?.enabled) {
626
- const LogViewerModule = window.slice.frameworkClasses?.LogViewer
627
- || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Logger/LogViewer/LogViewer.js`);
628
- const logViewer = new LogViewerModule();
629
- window.slice.logViewer = logViewer;
630
- await logViewer.init();
631
- logViewer.style.display = 'none';
632
- document.body.appendChild(logViewer);
633
- }
634
-
635
- if (sliceConfig.loading.enabled) {
636
- const loading = await window.slice.build('Loading', {});
637
- window.slice.loading = loading;
638
- if (typeof loading?.start === 'function') {
639
- loading.start();
640
- }
641
- }
642
-
643
- const stylesInitPromise = window.slice.stylesManager.init();
644
- const routesModulePromise = import(slice.paths.routesFile);
645
-
646
- if (sliceConfig.events?.ui?.shortcut || sliceConfig.context?.ui?.shortcut || sliceConfig.logger?.ui?.shortcut || sliceConfig.debugger?.leakInspector?.shortcut) {
647
- const normalize = (value) => (typeof value === 'string' ? value.toLowerCase() : '');
648
- const toKey = (event) => {
649
- const parts = [];
650
- if (event.ctrlKey) parts.push('ctrl');
651
- if (event.shiftKey) parts.push('shift');
652
- if (event.altKey) parts.push('alt');
653
- if (event.metaKey) parts.push('meta');
654
- const key = event.key?.toLowerCase();
655
- if (key && !['control', 'shift', 'alt', 'meta'].includes(key)) {
656
- parts.push(key);
657
- }
658
- return parts.join('+');
659
- };
660
-
661
- const handlers = {
662
- [normalize(sliceConfig.events?.ui?.shortcut)]: () => window.slice.eventsDebugger?.toggle?.(),
663
- [normalize(sliceConfig.context?.ui?.shortcut)]: () => window.slice.contextDebugger?.toggle?.(),
664
- [normalize(sliceConfig.logger?.ui?.shortcut)]: () => window.slice.logViewer?.toggle?.(),
665
- [normalize(sliceConfig.debugger?.leakInspector?.shortcut)]: () => window.slice.leakInspector?.toggle?.(),
666
- };
667
-
668
- document.addEventListener('keydown', (event) => {
669
- const key = toKey(event);
670
- if (!key || !handlers[key]) return;
671
- event.preventDefault();
672
- handlers[key]();
673
- });
674
- }
675
-
676
- const [, routesModule] = await Promise.all([stylesInitPromise, routesModulePromise]);
677
- const routes = routesModule.default;
678
- const RouterModule = window.slice.frameworkClasses?.Router
679
- || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Router/Router.js`);
680
- window.slice.router = new RouterModule(routes);
681
- await window.slice.router.init();
682
- }
683
-
684
- // Booting requires a DOM. Under node — where this module is imported for the
685
- // Slice class alone (see Slice/tests) — there is nothing to initialize, so skip
686
- // it rather than starting an init() that can only fail.
687
- if (typeof window !== 'undefined' && typeof document !== 'undefined') {
688
- // Exposed so callers that cannot rely on module import order — test
689
- // harnesses, embedders, anything loaded via a plain <script> — have a real
690
- // readiness signal. It settles exactly when init() does. Note that
691
- // `window.slice` and `slice.build` exist well before init() finishes, so
692
- // checking for those is NOT a readiness test; await this (or check
693
- // `slice.router`) instead.
694
- window.__SLICE_READY__ = init();
695
-
696
- try {
697
- await window.__SLICE_READY__;
698
- if (window.slice) window.slice.initError = null;
699
- } catch (initError) {
700
- // Deliberately not swallowed. A half-initialized runtime (no
701
- // slice.router, no registered components) makes every downstream call
702
- // fail with its own unrelated-looking TypeError — "Cannot read
703
- // properties of undefined (reading 'afterEach')", "appendChild:
704
- // parameter 1 is not of type 'Node'" — each of which hides this, the
705
- // actual cause. Record it, then rethrow so that importing this module
706
- // fails loudly and on the real error.
707
- if (window.slice) window.slice.initError = initError;
708
- console.error('[Slice.js] Initialization failed:', initError);
709
- throw initError;
710
- }
711
- }
1
+
2
+ /**
3
+ * Main Slice.js runtime.
4
+ */
5
+ export default class Slice {
6
+ /**
7
+ * @param {Object} sliceConfig
8
+ */
9
+ constructor(sliceConfig, frameworkClasses = null) {
10
+ this.frameworkClasses = frameworkClasses;
11
+ const ControllerClass = frameworkClasses?.Controller;
12
+ const StylesManagerClass = frameworkClasses?.StylesManager;
13
+
14
+ this.controller = ControllerClass ? new ControllerClass() : null;
15
+ this.stylesManager = StylesManagerClass ? new StylesManagerClass() : null;
16
+ this.paths = sliceConfig.paths;
17
+ this.themeConfig = sliceConfig.themeManager;
18
+ this.stylesConfig = sliceConfig.stylesManager;
19
+ this.loggerConfig = sliceConfig.logger;
20
+ this.debuggerConfig = sliceConfig.debugger;
21
+ this.loadingConfig = sliceConfig.loading;
22
+ this.eventsConfig = sliceConfig.events;
23
+ this.routerConfig = sliceConfig.router;
24
+ this.translatorConfig = sliceConfig.translator;
25
+
26
+ // Default to production until init() resolves the actual mode.
27
+ // Safe to call isProduction() before init() completes.
28
+ this._mode = 'production';
29
+ this._publicEnv = {};
30
+
31
+ // One translation entry point: `slice.t()` for text, `slice.t.ref()` for a prop
32
+ // that must follow the locale, `slice.t.list()` for a list, `slice.t.content()` for a
33
+ // content document. Bound here so all of them survive destructuring.
34
+ this.t = this.t.bind(this);
35
+ this.t.ref = (key, params) => (this.translator ? this.translator.ref(key, params) : String(key ?? ''));
36
+ this.t.ref.list = (key, params) => (this.translator ? this.translator.refList(key, params) : String(key ?? ''));
37
+ this.t.list = (key, params) => (this.translator ? this.translator.list(key, params) : []);
38
+ this.t.content = (name) => (this.translator ? this.translator.content(name) : Promise.resolve(null));
39
+ this.t.localized = (value) => (this.translator ? this.translator.localized(value) : value);
40
+
41
+ // 📦 Bundle system is initialized automatically via import in index.js
42
+ }
43
+
44
+ /**
45
+ * Dynamically import a module and return its default export.
46
+ * @param {string} module
47
+ * @returns {Promise<any>}
48
+ */
49
+ async getClass(module) {
50
+ try {
51
+ const { default: myClass } = await import(module);
52
+ return await myClass;
53
+ } catch (error) {
54
+ this.logger.error('Slice', `Error loading class ${module}`, error);
55
+ throw error;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Returns true when running in production mode.
61
+ * Reliable after init() has completed.
62
+ * @returns {boolean}
63
+ */
64
+ isProduction() {
65
+ return this._mode === 'production';
66
+ }
67
+
68
+ setPublicEnv(envPayload = {}) {
69
+ const normalized = {};
70
+
71
+ for (const [key, value] of Object.entries(envPayload || {})) {
72
+ if (!key.startsWith('SLICE_PUBLIC_')) continue;
73
+ normalized[key] = String(value ?? '');
74
+ }
75
+
76
+ this._publicEnv = normalized;
77
+ }
78
+
79
+ getEnv(name, fallbackValue = undefined) {
80
+ if (!name || typeof name !== 'string') {
81
+ return fallbackValue;
82
+ }
83
+
84
+ if (Object.prototype.hasOwnProperty.call(this._publicEnv, name)) {
85
+ return this._publicEnv[name];
86
+ }
87
+
88
+ return fallbackValue;
89
+ }
90
+
91
+ getPublicEnv() {
92
+ return { ...this._publicEnv };
93
+ }
94
+
95
+ /**
96
+ * Typed accessors over the public env, so callers stop re-parsing strings.
97
+ * slice.env.get('SLICE_PUBLIC_API_URL', '')
98
+ * slice.env.bool('SLICE_PUBLIC_AUTH_ENABLED') // '1'|'true'|'yes'|'on' → true
99
+ * slice.env.int('SLICE_PUBLIC_TIMEOUT', 5000)
100
+ * slice.env.list('SLICE_PUBLIC_MODELS') // 'a, b' → ['a','b']
101
+ * slice.env.has('X') / slice.env.all()
102
+ * @returns {{ get: Function, has: Function, all: Function, bool: Function, int: Function, list: Function }}
103
+ */
104
+ get env() {
105
+ const read = (name) =>
106
+ Object.prototype.hasOwnProperty.call(this._publicEnv, name) ? this._publicEnv[name] : undefined;
107
+ const present = (v) => v !== undefined && String(v).trim() !== '';
108
+
109
+ return {
110
+ get: (name, fallback = undefined) => this.getEnv(name, fallback),
111
+ has: (name) => Object.prototype.hasOwnProperty.call(this._publicEnv, name),
112
+ all: () => this.getPublicEnv(),
113
+ bool: (name, fallback = false) => {
114
+ const v = read(name);
115
+ return present(v) ? ['1', 'true', 'yes', 'on'].includes(String(v).trim().toLowerCase()) : fallback;
116
+ },
117
+ int: (name, fallback = 0) => {
118
+ const v = read(name);
119
+ const n = parseInt(v, 10);
120
+ return Number.isNaN(n) ? fallback : n;
121
+ },
122
+ list: (name, fallback = []) => {
123
+ const v = read(name);
124
+ if (!present(v)) return fallback;
125
+ return String(v)
126
+ .split(',')
127
+ .map((s) => s.trim())
128
+ .filter(Boolean);
129
+ },
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Get a component instance by sliceId.
135
+ * @param {string} componentSliceId
136
+ * @returns {HTMLElement|undefined}
137
+ */
138
+ getComponent(componentSliceId) {
139
+ return this.controller.activeComponents.get(componentSliceId);
140
+ }
141
+
142
+ /**
143
+ * Apply props to a component. The controller auto-detects build vs refresh:
144
+ * during construction (not yet registered) it applies defaults + required
145
+ * validation; once registered, it refreshes in place (no defaults, no required,
146
+ * respects diff-guards). Same call works for both.
147
+ * @param {HTMLElement} component
148
+ * @param {Object} props
149
+ * @returns {void}
150
+ */
151
+ setComponentProps(component, props) {
152
+ return this.controller.setComponentProps(component, props);
153
+ }
154
+
155
+ /**
156
+ * Whether `component` is still the live registered instance for its sliceId
157
+ * (false after it has been destroyed, or replaced by a rebuild).
158
+ * @param {HTMLElement} component
159
+ * @returns {boolean}
160
+ */
161
+ isComponentAlive(component) {
162
+ return !!component && this.controller.activeComponents.get(component.sliceId) === component;
163
+ }
164
+
165
+ /**
166
+ * Build a component instance and run init.
167
+ *
168
+ * Pass `{ singleton: true }` to get-or-create one shared instance keyed by
169
+ * `sliceId` (defaults to `componentName`). Concurrent singleton builds of the
170
+ * same id share a single in-flight build, so they never race on a duplicate
171
+ * sliceId. Singletons are only supported for Service components — for app-wide
172
+ * UI build a Provider Service that manages the Visual (see ToastProvider /
173
+ * ToolTipProvider), because a DOM node can only live in one place.
174
+ *
175
+ * Note: `props` only apply on the first (creating) call; later calls return
176
+ * the existing instance and ignore them.
177
+ *
178
+ * @param {string} componentName
179
+ * @param {Object} [props]
180
+ * @param {boolean} [props.singleton] Reuse a single instance per sliceId.
181
+ * @returns {Promise<HTMLElement|Object|null>}
182
+ */
183
+ async build(componentName, props = {}) {
184
+ if (!props || props.singleton !== true) {
185
+ return this._build(componentName, props);
186
+ }
187
+
188
+ const { singleton, ...rest } = props;
189
+ const sliceId = rest.sliceId || componentName;
190
+
191
+ // Singletons are allowed for any category whose *type* is 'Service' — not
192
+ // only the built-in 'Service' category. Custom categories declared in
193
+ // sliceConfig with `"type": "Service"` (e.g. AppServices) are services too.
194
+ const category = this.controller.componentCategories.get(componentName);
195
+ const categoryType = slice.paths?.components?.[category]?.type;
196
+ if (categoryType !== 'Service') {
197
+ this.logger.logError(
198
+ 'Slice',
199
+ `singleton:true is only supported for Service-type components ('${componentName}' is in category '${category || 'unknown'}', type '${categoryType || 'unknown'}'). ` +
200
+ `For app-wide UI build a Provider Service that manages the Visual (see ToastProvider/ToolTipProvider).`
201
+ );
202
+ return null;
203
+ }
204
+
205
+ return this.controller.getOrCreate(sliceId, () =>
206
+ this._build(componentName, { ...rest, sliceId })
207
+ );
208
+ }
209
+
210
+ /**
211
+ * Internal build: load resources, construct, run init, register. Always
212
+ * creates a new instance. Public `build` delegates here (and wraps it with
213
+ * get-or-create when `singleton:true`).
214
+ * @param {string} componentName
215
+ * @param {Object} [props]
216
+ * @returns {Promise<HTMLElement|Object|null>}
217
+ */
218
+ async _build(componentName, props = {}) {
219
+ if (!componentName) {
220
+ this.logger.error('Slice', 'Component name is required to build a component');
221
+ return null;
222
+ }
223
+
224
+ if (typeof componentName !== 'string') {
225
+ this.logger.error('Slice', 'Component name must be a string');
226
+ return null;
227
+ }
228
+
229
+ // 📦 Try to load from bundles first
230
+ const bundleName = this.controller.getBundleForComponent(componentName);
231
+ if (bundleName && !this.controller.loadedBundles.has(bundleName)) {
232
+ await this.controller.loadBundle(bundleName);
233
+ }
234
+
235
+ if (!this.controller.componentCategories.has(componentName) && !this.controller.classes.has(componentName)) {
236
+ this.logger.error('Slice', `Component ${componentName} not found in components.js file`);
237
+ return null;
238
+ }
239
+
240
+ let componentCategory = this.controller.componentCategories.get(componentName);
241
+ if (!componentCategory && this.controller.classes.has(componentName)) {
242
+ componentCategory = 'AppComponents';
243
+ }
244
+
245
+ // 📦 Check if component is already available from loaded bundles
246
+ const isFromBundle = this.controller.isComponentFromBundle(componentName);
247
+
248
+ if (componentCategory === 'Structural') {
249
+ this.logger.error('Slice', `Component ${componentName} is a Structural component and cannot be built`);
250
+ return null;
251
+ }
252
+
253
+ let isVisual = slice.paths.components[componentCategory]?.type === 'Visual';
254
+ let modulePath = `${slice.paths.components[componentCategory].path}/${componentName}/${componentName}.js`;
255
+ const isJsOnlyVisualComponent = isVisual && (componentName === 'MultiRoute' || componentName === 'Route');
256
+
257
+ // Load template, class, and CSS concurrently if needed
258
+ try {
259
+ // 📦 Skip individual loading if component is available from bundles
260
+ const loadTemplate =
261
+ isFromBundle || !isVisual || isJsOnlyVisualComponent || this.controller.templates.has(componentName)
262
+ ? Promise.resolve(null)
263
+ : this.controller.fetchText(componentName, 'html', componentCategory);
264
+
265
+ const loadClass =
266
+ isFromBundle || this.controller.classes.has(componentName)
267
+ ? Promise.resolve(null)
268
+ : this.getClass(modulePath);
269
+
270
+ const loadCSS =
271
+ isFromBundle || !isVisual || isJsOnlyVisualComponent || this.controller.requestedStyles.has(componentName)
272
+ ? Promise.resolve(null)
273
+ : this.controller.fetchText(componentName, 'css', componentCategory);
274
+
275
+ const [html, ComponentClass, css] = await Promise.all([loadTemplate, loadClass, loadCSS]);
276
+
277
+ // 📦 If component is from bundle but not in cache, it should have been registered by registerBundle
278
+ if (isFromBundle) {
279
+ this.logger.logInfo('Slice', `📦 Using bundled component: ${componentName}`);
280
+ }
281
+
282
+ if (html || html === '') {
283
+ const template = document.createElement('template');
284
+ template.innerHTML = html;
285
+ this.controller.templates.set(componentName, template);
286
+ this.logger.logInfo('Slice', `Template ${componentName} loaded`);
287
+ }
288
+
289
+ if (ComponentClass) {
290
+ this.controller.classes.set(componentName, ComponentClass);
291
+ this.logger.logInfo('Slice', `Class ${componentName} loaded`);
292
+ }
293
+
294
+ if (css) {
295
+ this.stylesManager.registerComponentStyles(componentName, css);
296
+ this.logger.logInfo('Slice', `CSS ${componentName} loaded`);
297
+ }
298
+ } catch (error) {
299
+ this.logger.logError('Slice', `Error loading resources for ${componentName}`, error);
300
+ return null;
301
+ }
302
+
303
+ // Create instance
304
+ try {
305
+ let componentIds = {};
306
+ if (props.id) componentIds.id = props.id;
307
+ if (props.sliceId) componentIds.sliceId = props.sliceId;
308
+
309
+ delete props.id;
310
+ delete props.sliceId;
311
+ // `singleton` is a build directive (handled in the public build()
312
+ // wrapper). Strip it here too so it is consistently reserved and never
313
+ // leaks into a component's props on the non-singleton path.
314
+ delete props.singleton;
315
+
316
+ const ComponentClass = this.controller.classes.get(componentName);
317
+ this.logger.logInfo(
318
+ 'Slice',
319
+ `🔎 Build component: ${componentName} (classType: ${typeof ComponentClass}, isFunction: ${typeof ComponentClass === 'function'})`
320
+ );
321
+ const componentInstance = new ComponentClass(props);
322
+
323
+ if (componentIds.id && isVisual) componentInstance.id = componentIds.id;
324
+ if (componentIds.sliceId) componentInstance.sliceId = componentIds.sliceId;
325
+
326
+ if (!this.controller.verifyComponentIds(componentInstance)) {
327
+ this.logger.logError('Slice', `Error registering instance ${componentName} ${componentInstance.sliceId}`);
328
+ return null;
329
+ }
330
+
331
+ // Register before init so the component is visible in activeComponents
332
+ // during init(). This lets slice.router.navigate() work from inside
333
+ // init() without the Router creating a duplicate instance (the Router
334
+ // finds the existing component via getComponent). Calling update()
335
+ // from init() is safe because installUpdate already wrapped it.
336
+ this.controller.registerComponent(componentInstance);
337
+
338
+ if (componentInstance.init) await componentInstance.init();
339
+
340
+ if (slice.debuggerConfig.enabled && isVisual) {
341
+ this.debugger.attachDebugMode(componentInstance);
342
+ }
343
+
344
+ if (isVisual) {
345
+ this.controller.registerComponentsRecursively(componentInstance);
346
+ }
347
+
348
+ this.logger.logInfo('Slice', `Instance ${componentInstance.sliceId} created`);
349
+ return componentInstance;
350
+ } catch (error) {
351
+ this.logger.logError('Slice', `Error creating instance ${componentName}`, error);
352
+ return null;
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Apply a theme by name.
358
+ * @param {string} themeName
359
+ * @returns {Promise<void>}
360
+ */
361
+ async setTheme(themeName) {
362
+ await this.stylesManager.themeManager.applyTheme(themeName);
363
+ }
364
+
365
+ /**
366
+ * Current theme name.
367
+ * @returns {string|null}
368
+ */
369
+ get theme() {
370
+ return this.stylesManager.themeManager.currentTheme;
371
+ }
372
+
373
+ /**
374
+ * Translate a key with slice.translator (shortcut of `slice.translator.t`).
375
+ * Returns the key itself when the translator is off or the key is missing.
376
+ * @param {string} key
377
+ * @param {Object} [params]
378
+ * @returns {string}
379
+ */
380
+ t(key, params) {
381
+ return this.translator ? this.translator.t(key, params) : String(key ?? '');
382
+ }
383
+
384
+ /**
385
+ * The active locale (shortcut of `slice.translator.locale`).
386
+ * @returns {string}
387
+ */
388
+ get locale() {
389
+ if (this.translator?.locale) return this.translator.locale;
390
+ return typeof document !== 'undefined' ? document.documentElement.lang || '' : '';
391
+ }
392
+
393
+ /**
394
+ * The app path the router is showing, without locale or query: `/pricing`.
395
+ * Falls back to the browser path before the router exists. Components use this
396
+ * instead of `window.location.pathname`.
397
+ * @returns {string}
398
+ */
399
+ get path() {
400
+ const path = this.router?.path;
401
+ if (typeof path === 'string' && path) return path;
402
+ return typeof window !== 'undefined' ? window.location.pathname : '/';
403
+ }
404
+
405
+ /**
406
+ * The URL to render in an `<a href>` for an app path: `/pricing` → `/en/pricing` when
407
+ * the locale is in the URL, unchanged otherwise.
408
+ * @param {string} path
409
+ * @param {{ locale?: string }} [options]
410
+ * @returns {string}
411
+ */
412
+ href(path, options) {
413
+ const value = path == null ? '' : String(path);
414
+ return typeof this.router?.href === 'function' ? this.router.href(value, options) : value;
415
+ }
416
+
417
+ /**
418
+ * Attach HTML template to a component instance.
419
+ * @param {HTMLElement} componentInstance
420
+ * @returns {void}
421
+ */
422
+ attachTemplate(componentInstance) {
423
+ this.controller.loadTemplateToComponent(componentInstance);
424
+ }
425
+ }
426
+
427
+ /**
428
+ * `/en/pricing` → `/pricing` when `translator.routing` is on and `en` is a configured locale.
429
+ * @param {string} pathname
430
+ * @param {Object} [translatorConfig]
431
+ * @returns {string}
432
+ */
433
+ function stripLocalePrefix(pathname, translatorConfig) {
434
+ const localePrefix = translatorConfig?.localePrefix;
435
+ if (!translatorConfig?.enabled || (localePrefix !== 'always' && localePrefix !== 'except-default')) return pathname;
436
+ const locales = [
437
+ translatorConfig.defaultLocale,
438
+ ...(translatorConfig.locales || []),
439
+ translatorConfig.fallbackLocale
440
+ ]
441
+ .filter((locale) => typeof locale === 'string')
442
+ .map((locale) => locale.toLowerCase());
443
+ const match = /^\/([^/]+)(\/.*)?$/.exec(pathname);
444
+ return match && locales.includes(match[1].toLowerCase()) ? match[2] || '/' : pathname;
445
+ }
446
+
447
+ async function loadConfig() {
448
+ try {
449
+ const response = await fetch('/sliceConfig.json');
450
+ if (!response.ok) throw new Error('Error loading sliceConfig.json');
451
+ const json = await response.json();
452
+ return json;
453
+ } catch (error) {
454
+ console.error('Error loading config file:', error);
455
+ return null;
456
+ }
457
+ }
458
+
459
+ async function init() {
460
+ const sliceConfig = await loadConfig();
461
+ if (!sliceConfig) {
462
+ console.error('%c\u26A0\uFE0F Error loading Slice configuration', 'color: red; font-size: 20px;');
463
+ alert('Error loading Slice configuration');
464
+ throw new Error('Slice initialization failed: unable to load sliceConfig.json');
465
+ }
466
+
467
+ // 1+2. Fetch mode endpoint and bundle config in parallel — both are independent.
468
+ // In production, /slice-env.json returns 404 (catch is expected and normal).
469
+ // bundleConfigJson.production serves as a mode fallback when env endpoint is absent.
470
+ let frameworkClasses = null;
471
+ const [envResult, configResult] = await Promise.all([
472
+ fetch('/slice-env.json', { cache: 'no-store' })
473
+ .then(r => r.ok ? r.json() : null)
474
+ .catch(error => { console.error('[Slice.js] Error fetching /slice-env.json:', error); return null; }),
475
+ fetch('/bundles/bundle.config.json', { cache: 'no-store' })
476
+ .then(r => r.ok ? r.json() : null)
477
+ .catch(error => { console.error('[Slice.js] Error fetching bundle.config.json:', error); return null; })
478
+ ]);
479
+ const envMode = envResult?.mode ?? null;
480
+ const bundleConfigJson = configResult;
481
+
482
+ // 3. Determine canonical mode: env endpoint takes precedence, then bundle config
483
+ let resolvedMode;
484
+ if (envMode) {
485
+ resolvedMode = envMode;
486
+ } else if (bundleConfigJson?.production) {
487
+ resolvedMode = 'production';
488
+ } else {
489
+ resolvedMode = 'development';
490
+ }
491
+
492
+ // 4. Load framework classes.
493
+ // In production the bundler generates slice-bundle.framework.js which
494
+ // sets window.SLICE_FRAMEWORK_CLASSES. In dev mode always use individual
495
+ // imports so the live /Slice/ source is served directly without bundles.
496
+ if (resolvedMode === 'production' && bundleConfigJson?.bundles?.framework?.file) {
497
+ try {
498
+ await import(`/bundles/${bundleConfigJson.bundles.framework.file}`);
499
+ if (window.SLICE_FRAMEWORK_CLASSES) {
500
+ frameworkClasses = window.SLICE_FRAMEWORK_CLASSES;
501
+ }
502
+ } catch (e) {
503
+ console.error('[Slice.js] framework bundle import failed, falling through to individual imports:', e);
504
+ }
505
+ }
506
+
507
+ if (!frameworkClasses) {
508
+ try {
509
+ const imports = await Promise.all([
510
+ import('./Components/Structural/Controller/Controller.js'),
511
+ import('./Components/Structural/StylesManager/StylesManager.js')
512
+ ]);
513
+ frameworkClasses = {
514
+ Controller: imports[0].default,
515
+ StylesManager: imports[1].default
516
+ };
517
+ } catch (e) {
518
+ console.error('[Slice.js] individual imports fallback failed:', e);
519
+ throw e;
520
+ }
521
+ }
522
+
523
+ // 5. Create Slice instance and set resolved mode
524
+ window.slice = new Slice(sliceConfig, frameworkClasses);
525
+ window.slice._mode = resolvedMode;
526
+ window.slice.setPublicEnv(envResult?.env || {});
527
+
528
+ slice.paths.structuralComponentFolderPath = '/Slice/Components/Structural';
529
+
530
+ // Logger must be available before bundle loading — Controller internally calls
531
+ // slice.logger.* methods during loadBundle/registerBundle.
532
+ if (sliceConfig.logger.enabled) {
533
+ const LoggerModule = window.slice.frameworkClasses?.Logger
534
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Logger/Logger.js`);
535
+ window.slice.logger = new LoggerModule();
536
+ } else {
537
+ const noop = () => {};
538
+ window.slice.logger = {
539
+ error: noop, warn: noop, info: noop, debug: noop,
540
+ logError: noop, logWarning: noop, logInfo: noop,
541
+ };
542
+ }
543
+
544
+ const createBundlingInitError = (step, error) => {
545
+ const detail = error instanceof Error ? error.message : String(error);
546
+ return new Error(`Bundling V2 initialization failed (${step}): ${detail}`, { cause: error });
547
+ };
548
+
549
+ // Initialize bundles before building components.
550
+ // Only in production — dev mode loads each component individually from source.
551
+ // bundleConfigJson was already fetched above (step 2); reuse it.
552
+ if (resolvedMode === 'production' && bundleConfigJson) {
553
+ window.slice.controller.bundleConfig = bundleConfigJson;
554
+ }
555
+
556
+ if (resolvedMode === 'production' && window.slice.controller.bundleConfig) {
557
+ const config = window.slice.controller.bundleConfig;
558
+ if (!window.__SLICE_SHARED_DEPS__ || typeof window.__SLICE_SHARED_DEPS__ !== 'object') {
559
+ window.__SLICE_SHARED_DEPS__ = {};
560
+ }
561
+ const criticalFile = config?.bundles?.critical?.file;
562
+ if (criticalFile) {
563
+ try {
564
+ await window.slice.controller.loadBundle('critical');
565
+ } catch (error) {
566
+ throw createBundlingInitError(`critical bundle "${criticalFile}"`, error);
567
+ }
568
+ }
569
+
570
+ const routeBundles = config?.routeBundles || {};
571
+ // Route bundles are keyed by app path: drop a locale prefix (`/en/pricing`) when
572
+ // the translator puts the locale in the URL. (The translator is not created yet.)
573
+ const initialPath = stripLocalePrefix(window.location.pathname || '/', sliceConfig.translator);
574
+ const bundlesForRoute = routeBundles[initialPath] || [];
575
+
576
+ const loadRouteBundles = async () => {
577
+ for (const bundleName of bundlesForRoute) {
578
+ if (bundleName === 'critical') continue;
579
+ const bundleInfo = config?.bundles?.routes?.[bundleName];
580
+ if (!bundleInfo?.file) continue;
581
+ await window.slice.controller.loadBundle(bundleName);
582
+ }
583
+ };
584
+
585
+ const preloadRouteBundles = () => {
586
+ loadRouteBundles().catch((error) => {
587
+ window.slice?.logger?.error('Slice', `Idle route preload failed for "${initialPath}"`, error);
588
+ });
589
+ };
590
+
591
+ const safePreload = () => {
592
+ try {
593
+ preloadRouteBundles();
594
+ } catch (error) {
595
+ window.slice?.logger?.error('Slice', 'Error in route preload callback', error);
596
+ }
597
+ };
598
+
599
+ if (typeof requestIdleCallback === 'function') {
600
+ requestIdleCallback(() => safePreload());
601
+ } else {
602
+ setTimeout(() => safePreload(), 0);
603
+ }
604
+ }
605
+
606
+ // Translator: created once the logger and the critical bundle are in, so its dictionaries
607
+ // download while the rest of the runtime starts. Always present (with `enabled: false` it loads nothing
608
+ // and `slice.t()` returns the key), so translated code runs in any app.
609
+ const TranslatorModule = window.slice.frameworkClasses?.Translator
610
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Translator/Translator.js`);
611
+ window.slice.translator = new TranslatorModule(sliceConfig.translator);
612
+ const translatorReady = window.slice.translator.init();
613
+
614
+ if (sliceConfig.debugger.enabled) {
615
+ const DebuggerModule = window.slice.frameworkClasses?.Debugger
616
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Debugger/Debugger.js`);
617
+ window.slice.debugger = new DebuggerModule();
618
+ await window.slice.debugger.enableDebugMode();
619
+ document.body.appendChild(window.slice.debugger);
620
+ }
621
+
622
+ if (sliceConfig.events?.ui?.enabled) {
623
+ const EventsDebuggerModule = window.slice.frameworkClasses?.EventManagerDebugger
624
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/EventManager/EventManagerDebugger.js`);
625
+ window.slice.eventsDebugger = new EventsDebuggerModule();
626
+ await window.slice.eventsDebugger.init();
627
+ document.body.appendChild(window.slice.eventsDebugger);
628
+ }
629
+
630
+ if (sliceConfig.context?.ui?.enabled) {
631
+ const ContextDebuggerModule = window.slice.frameworkClasses?.ContextManagerDebugger
632
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/ContextManager/ContextManagerDebugger.js`);
633
+ window.slice.contextDebugger = new ContextDebuggerModule();
634
+ await window.slice.contextDebugger.init();
635
+ document.body.appendChild(window.slice.contextDebugger);
636
+ }
637
+
638
+ if (sliceConfig.translator?.ui?.enabled) {
639
+ const TranslatorDebuggerModule = window.slice.frameworkClasses?.TranslatorDebugger
640
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Translator/TranslatorDebugger.js`);
641
+ window.slice.translatorDebugger = new TranslatorDebuggerModule();
642
+ await window.slice.translatorDebugger.init();
643
+ document.body.appendChild(window.slice.translatorDebugger);
644
+ }
645
+
646
+ if (sliceConfig.debugger?.leakInspector?.enabled && !window.slice.isProduction()) {
647
+ const LeakInspectorModule = window.slice.frameworkClasses?.LeakInspector
648
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/LeakInspector/LeakInspector.js`);
649
+ window.slice.leakInspector = new LeakInspectorModule();
650
+ await window.slice.leakInspector.init();
651
+ document.body.appendChild(window.slice.leakInspector);
652
+ }
653
+
654
+ // ContextManager delivers setState → watch through EventManager, so context
655
+ // reactivity requires events. We don't auto-enable events (in a production
656
+ // build EventManager is only bundled when events.enabled was true, so it may
657
+ // not have shipped) nor throw (a boot-time throw would take down the whole
658
+ // app, inconsistent with how "EventManager disabled" is handled below). Log a
659
+ // loud error and continue; `slice build` hard-fails on this same misconfig so
660
+ // a broken bundle never ships.
661
+ if (sliceConfig.context?.enabled && !sliceConfig.events?.enabled) {
662
+ window.slice.logger.logError(
663
+ 'Slice',
664
+ 'context.enabled requires events.enabled — ContextManager reactivity (setState → watch/bind) is delivered through EventManager. With events disabled, watchers will not fire. Set "events": { "enabled": true } in sliceConfig.json.'
665
+ );
666
+ }
667
+
668
+ if (sliceConfig.events?.enabled) {
669
+ const EventManagerModule = window.slice.frameworkClasses?.EventManager
670
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/EventManager/EventManager.js`);
671
+ window.slice.events = new EventManagerModule();
672
+ if (typeof window.slice.events.init === 'function') {
673
+ await window.slice.events.init();
674
+ }
675
+ } else {
676
+ window.slice.events = {
677
+ subscribe: () => null,
678
+ subscribeOnce: () => null,
679
+ unsubscribe: () => false,
680
+ emit: () => {},
681
+ register: () => window.slice.events,
682
+ loadGraph: () => window.slice.events,
683
+ isDeclared: () => false,
684
+ bind: () => ({
685
+ subscribe: () => null,
686
+ subscribeOnce: () => null,
687
+ emit: () => {},
688
+ register: () => window.slice.events,
689
+ }),
690
+ cleanupComponent: () => 0,
691
+ hasSubscribers: () => false,
692
+ subscriberCount: () => 0,
693
+ clear: () => {},
694
+ };
695
+ window.slice.logger.logError('Slice', 'EventManager disabled');
696
+ }
697
+
698
+ if (sliceConfig.context?.enabled) {
699
+ const ContextManagerModule = window.slice.frameworkClasses?.ContextManager
700
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/ContextManager/ContextManager.js`);
701
+ window.slice.context = new ContextManagerModule();
702
+ if (typeof window.slice.context.init === 'function') {
703
+ await window.slice.context.init();
704
+ }
705
+ } else {
706
+ window.slice.context = {
707
+ create: () => false,
708
+ getState: () => null,
709
+ setState: () => {},
710
+ watch: () => null,
711
+ has: () => false,
712
+ destroy: () => false,
713
+ list: () => [],
714
+ };
715
+ window.slice.logger.logError('Slice', 'ContextManager disabled');
716
+ }
717
+
718
+ if (sliceConfig.logger?.ui?.enabled) {
719
+ const LogViewerModule = window.slice.frameworkClasses?.LogViewer
720
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Logger/LogViewer/LogViewer.js`);
721
+ const logViewer = new LogViewerModule();
722
+ window.slice.logViewer = logViewer;
723
+ await logViewer.init();
724
+ logViewer.style.display = 'none';
725
+ document.body.appendChild(logViewer);
726
+ }
727
+
728
+ if (sliceConfig.loading.enabled) {
729
+ const loading = await window.slice.build('Loading', {});
730
+ window.slice.loading = loading;
731
+ if (typeof loading?.start === 'function') {
732
+ loading.start();
733
+ }
734
+ }
735
+
736
+ const stylesInitPromise = window.slice.stylesManager.init();
737
+ const routesModulePromise = import(slice.paths.routesFile);
738
+
739
+ if (sliceConfig.events?.ui?.shortcut || sliceConfig.context?.ui?.shortcut || sliceConfig.translator?.ui?.shortcut || sliceConfig.logger?.ui?.shortcut || sliceConfig.debugger?.leakInspector?.shortcut) {
740
+ const normalize = (value) => {
741
+ if (typeof value !== 'string') return '';
742
+ const order = ['ctrl', 'alt', 'shift', 'meta'];
743
+ const parts = value.toLowerCase().split('+').map((part) => part.trim()).filter(Boolean);
744
+ const modifiers = parts
745
+ .filter((part) => order.includes(part))
746
+ .sort((a, b) => order.indexOf(a) - order.indexOf(b));
747
+ const key = parts.find((part) => !order.includes(part)) || '';
748
+ return [...modifiers, key].filter(Boolean).join('+');
749
+ };
750
+ const toKey = (event) => {
751
+ const parts = [];
752
+ if (event.ctrlKey) parts.push('ctrl');
753
+ if (event.altKey) parts.push('alt');
754
+ if (event.shiftKey) parts.push('shift');
755
+ if (event.metaKey) parts.push('meta');
756
+ const key = event.key?.toLowerCase();
757
+ if (key && !['control', 'shift', 'alt', 'meta'].includes(key)) {
758
+ parts.push(key);
759
+ }
760
+ return parts.join('+');
761
+ };
762
+
763
+ const handlers = {};
764
+ const register = (shortcut, fn, label) => {
765
+ const combo = normalize(shortcut);
766
+ if (!combo) return;
767
+ if (handlers[combo]) {
768
+ console.warn(`[slice] The ${label} debugger shortcut "${combo}" is already used; the first one wins.`);
769
+ return;
770
+ }
771
+ handlers[combo] = fn;
772
+ };
773
+ if (sliceConfig.events?.ui?.enabled) register(sliceConfig.events.ui.shortcut, () => window.slice.eventsDebugger?.toggle?.(), 'events');
774
+ if (sliceConfig.context?.ui?.enabled) register(sliceConfig.context.ui.shortcut, () => window.slice.contextDebugger?.toggle?.(), 'context');
775
+ if (sliceConfig.translator?.ui?.enabled) register(sliceConfig.translator.ui.shortcut, () => window.slice.translatorDebugger?.toggle?.(), 'translator');
776
+ if (sliceConfig.logger?.ui?.enabled) register(sliceConfig.logger.ui.shortcut, () => window.slice.logViewer?.toggle?.(), 'logger');
777
+ if (sliceConfig.debugger?.leakInspector?.enabled) register(sliceConfig.debugger.leakInspector.shortcut, () => window.slice.leakInspector?.toggle?.(), 'leak inspector');
778
+
779
+ document.addEventListener('keydown', (event) => {
780
+ if (event.repeat) return;
781
+ const target = event.target;
782
+ if (target instanceof Element && target.closest('input, textarea, select, [contenteditable="true"]')) return;
783
+ const key = toKey(event);
784
+ if (!key || !handlers[key]) return;
785
+ event.preventDefault();
786
+ handlers[key]();
787
+ });
788
+ }
789
+
790
+ // The first route renders with its dictionaries loaded (no keys on the first paint).
791
+ const [, routesModule] = await Promise.all([stylesInitPromise, routesModulePromise, translatorReady]);
792
+ const routes = routesModule.default;
793
+ const RouterModule = window.slice.frameworkClasses?.Router
794
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Router/Router.js`);
795
+ window.slice.router = new RouterModule(routes);
796
+ await window.slice.router.init();
797
+ }
798
+
799
+ // Booting requires a DOM. Under node — where this module is imported for the
800
+ // Slice class alone (see Slice/tests) — there is nothing to initialize, so skip
801
+ // it rather than starting an init() that can only fail.
802
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
803
+ // Exposed so callers that cannot rely on module import order — test
804
+ // harnesses, embedders, anything loaded via a plain <script> — have a real
805
+ // readiness signal. It settles exactly when init() does. Note that
806
+ // `window.slice` and `slice.build` exist well before init() finishes, so
807
+ // checking for those is NOT a readiness test; await this (or check
808
+ // `slice.router`) instead.
809
+ window.__SLICE_READY__ = init();
810
+
811
+ try {
812
+ await window.__SLICE_READY__;
813
+ if (window.slice) window.slice.initError = null;
814
+ } catch (initError) {
815
+ // Deliberately not swallowed. A half-initialized runtime (no
816
+ // slice.router, no registered components) makes every downstream call
817
+ // fail with its own unrelated-looking TypeError — "Cannot read
818
+ // properties of undefined (reading 'afterEach')", "appendChild:
819
+ // parameter 1 is not of type 'Node'" — each of which hides this, the
820
+ // actual cause. Record it, then rethrow so that importing this module
821
+ // fails loudly and on the real error.
822
+ if (window.slice) window.slice.initError = initError;
823
+ console.error('[Slice.js] Initialization failed:', initError);
824
+ throw initError;
825
+ }
826
+ }