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