slicejs-web-framework 3.3.7 → 3.4.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.
@@ -1,1199 +1,1198 @@
1
- import components from '/Components/components.js';
2
- import { collectInvalidAllowedValueProps, formatAllowedValuesForLog } from './allowedValuesValidation.js';
3
-
4
- export default class Controller {
5
- constructor() {
6
- this.componentCategories = new Map(Object.entries(components));
7
- this.templates = new Map();
8
- this.classes = new Map();
9
- this.requestedStyles = new Set(); // ✅ CRÍTICO: Para tracking de CSS cargados
10
- this.activeComponents = new Map();
11
-
12
- // 🚀 OPTIMIZACIÓN: Índice inverso para búsqueda rápida de hijos
13
- // parentSliceId → Set<childSliceId>
14
- this.childrenIndex = new Map();
15
-
16
- // 📦 Bundle system
17
- this.loadedBundles = new Set();
18
- this.bundleConfig = null;
19
- this.criticalBundleLoaded = false;
20
- this.bundleImportPromises = new Map();
21
- this.bundleLoadPromises = new Map();
22
-
23
- // 🔁 Singleton builds in flight: sliceId → Promise<instance>.
24
- // Lets concurrent build({singleton:true}) calls share one build instead
25
- // of racing on a duplicate sliceId. Entries are transient (deleted on settle).
26
- this._pendingBuilds = new Map();
27
-
28
- this.idCounter = 0;
29
- }
30
-
31
- /**
32
- * 📦 Initializes bundle system (called automatically when config is loaded)
33
- */
34
- initializeBundles(config = null) {
35
- if (config) {
36
- this.bundleConfig = config;
37
-
38
- // Register critical bundle components if available
39
- if (config.bundles?.critical) {
40
- // Critical bundle will be loaded explicitly
41
- }
42
- this.criticalBundleLoaded = false;
43
- } else {
44
- // No bundles available, will use individual component loading
45
- this.bundleConfig = null;
46
- this.criticalBundleLoaded = false;
47
- }
48
- }
49
-
50
- /**
51
- * Import a bundle URL once per page session.
52
- * Reuses the same Promise for concurrent callers.
53
- * @param {string} bundlePath
54
- * @returns {Promise<any>}
55
- */
56
- importBundleOnce(bundlePath) {
57
- if (!bundlePath) {
58
- return Promise.reject(new Error('Bundle path is required'));
59
- }
60
-
61
- if (this.bundleImportPromises.has(bundlePath)) {
62
- return this.bundleImportPromises.get(bundlePath);
63
- }
64
-
65
- const importPromise = import(bundlePath).catch((error) => {
66
- this.bundleImportPromises.delete(bundlePath);
67
- throw error;
68
- });
69
-
70
- this.bundleImportPromises.set(bundlePath, importPromise);
71
- return importPromise;
72
- }
73
-
74
- buildBundleImportPath(bundleInfo) {
75
- if (!bundleInfo || typeof bundleInfo.file !== 'string' || bundleInfo.file.length === 0) {
76
- throw new Error('Bundle file is required');
77
- }
78
-
79
- const basePath = `/bundles/${bundleInfo.file}`;
80
- const bundleHash = typeof bundleInfo.hash === 'string' ? bundleInfo.hash.trim() : '';
81
- if (!bundleHash) {
82
- return basePath;
83
- }
84
-
85
- return `${basePath}?v=${encodeURIComponent(bundleHash)}`;
86
- }
87
-
88
- /**
89
- * Validate Bundling V2 module contract.
90
- * Requires named exports: SLICE_BUNDLE_META and registerAll.
91
- * @param {any} bundleModule
92
- * @param {string} [bundleName]
93
- * @returns {{metadata: object, registerAll: Function}}
94
- */
95
- async validateBundleModule(bundleModule, bundleName = 'unknown') {
96
- const metadata = bundleModule?.SLICE_BUNDLE_META;
97
- const registerAll = bundleModule?.registerAll;
98
-
99
- if (!metadata || typeof metadata !== 'object' || typeof registerAll !== 'function') {
100
- throw new Error(
101
- `Bundle "${bundleName}" missing Bundling V2 exports contract: requires SLICE_BUNDLE_META and registerAll`
102
- );
103
- }
104
-
105
- return { metadata, registerAll };
106
- }
107
-
108
- /**
109
- * 📦 Loads a bundle by name or category
110
- */
111
- async loadBundle(bundleName) {
112
- const resolvedBundleName = this.resolveBundleName(bundleName);
113
- if (this.loadedBundles.has(resolvedBundleName)) {
114
- return; // Already loaded
115
- }
116
-
117
- return this.loadBundleWithDependencies(resolvedBundleName, new Set());
118
- }
119
-
120
- async loadBundleWithDependencies(bundleName, loadingStack = new Set()) {
121
- const resolvedBundleName = this.resolveBundleName(bundleName);
122
-
123
- if (this.loadedBundles.has(resolvedBundleName)) {
124
- return;
125
- }
126
-
127
- if (loadingStack.has(resolvedBundleName)) {
128
- throw new Error(`Circular bundle dependency detected: ${Array.from(loadingStack).join(' -> ')} -> ${resolvedBundleName}`);
129
- }
130
-
131
- if (this.bundleLoadPromises.has(resolvedBundleName)) {
132
- return this.bundleLoadPromises.get(resolvedBundleName);
133
- }
134
-
135
- const loadPromise = (async () => {
136
- loadingStack.add(resolvedBundleName);
137
- try {
138
- const bundleInfo = this.getBundleInfo(resolvedBundleName);
139
- if (!bundleInfo) {
140
- slice.logger.logWarning('Controller', `Bundle ${resolvedBundleName} not found in configuration`);
141
- return;
142
- }
143
-
144
- const dependencies = this.getBundleDependencies(bundleInfo);
145
- for (const dependencyName of dependencies) {
146
- await this.loadBundleWithDependencies(dependencyName, loadingStack);
147
- }
148
-
149
- const bundlePath = this.buildBundleImportPath(bundleInfo);
150
- const bundleModule = await this.importBundleOnce(bundlePath);
151
- const { metadata, registerAll } = await this.validateBundleModule(bundleModule, resolvedBundleName);
152
-
153
- const registerResult = await registerAll(this, slice.stylesManager);
154
- this.registerVendorSharedDependencies(bundleModule, metadata, resolvedBundleName, registerResult);
155
-
156
- this.loadedBundles.add(resolvedBundleName);
157
- const loadedBundleKey = metadata.bundleKey;
158
- if (loadedBundleKey && loadedBundleKey !== resolvedBundleName) {
159
- this.loadedBundles.add(loadedBundleKey);
160
- }
161
-
162
- if (metadata.type === 'critical' || resolvedBundleName === 'critical') {
163
- this.criticalBundleLoaded = true;
164
- }
165
- } finally {
166
- loadingStack.delete(resolvedBundleName);
167
- }
168
- })();
169
-
170
- this.bundleLoadPromises.set(resolvedBundleName, loadPromise);
171
- try {
172
- return await loadPromise;
173
- } finally {
174
- this.bundleLoadPromises.delete(resolvedBundleName);
175
- }
176
- }
177
-
178
- resolveBundleName(bundleName) {
179
- if (typeof bundleName !== 'string' || bundleName.length === 0) {
180
- return bundleName;
181
- }
182
-
183
- if (bundleName.toLowerCase() === 'critical') {
184
- return 'critical';
185
- }
186
-
187
- if (this.isVendorSharedAlias(bundleName) && this.getVendorSharedBundleInfo()) {
188
- return 'vendor-shared';
189
- }
190
-
191
- const routeBundleName = this.findBundleNameByAlias(this.bundleConfig?.bundles?.routes, bundleName);
192
- if (routeBundleName) {
193
- return routeBundleName;
194
- }
195
-
196
- const sharedBundleName = this.findBundleNameByAlias(this.bundleConfig?.bundles?.shared, bundleName);
197
- if (sharedBundleName) {
198
- return sharedBundleName;
199
- }
200
-
201
- return bundleName;
202
- }
203
-
204
- findBundleNameByAlias(bundleRegistry, bundleName) {
205
- if (!bundleRegistry || typeof bundleRegistry !== 'object') {
206
- return null;
207
- }
208
-
209
- if (bundleRegistry[bundleName]) {
210
- return bundleName;
211
- }
212
-
213
- const normalizedName = bundleName?.toLowerCase();
214
- if (!normalizedName) {
215
- return null;
216
- }
217
-
218
- return Object.keys(bundleRegistry).find((key) => key.toLowerCase() === normalizedName) || null;
219
- }
220
-
221
- getBundleDependencies(bundleInfo) {
222
- if (!bundleInfo || !Array.isArray(bundleInfo.dependencies)) {
223
- return [];
224
- }
225
-
226
- return bundleInfo.dependencies.filter((dependency) => typeof dependency === 'string' && dependency.length > 0);
227
- }
228
-
229
- findBundleEntryByName(bundleRegistry, bundleName) {
230
- if (!bundleRegistry || typeof bundleRegistry !== 'object') {
231
- return null;
232
- }
233
-
234
- if (bundleRegistry[bundleName]) {
235
- return bundleRegistry[bundleName];
236
- }
237
-
238
- const normalizedName = bundleName?.toLowerCase();
239
- if (!normalizedName) {
240
- return null;
241
- }
242
-
243
- const matchedKey = Object.keys(bundleRegistry).find((key) => key.toLowerCase() === normalizedName);
244
- return matchedKey ? bundleRegistry[matchedKey] : null;
245
- }
246
-
247
- getBundleInfo(bundleName) {
248
- if (bundleName === 'critical') {
249
- return this.bundleConfig?.bundles?.critical || null;
250
- }
251
-
252
- if (this.isVendorSharedAlias(bundleName)) {
253
- return this.getVendorSharedBundleInfo();
254
- }
255
-
256
- return (
257
- this.findBundleEntryByName(this.bundleConfig?.bundles?.routes, bundleName)
258
- || this.findBundleEntryByName(this.bundleConfig?.bundles?.shared, bundleName)
259
- );
260
- }
261
-
262
- getVendorSharedBundleInfo() {
263
- if (this.bundleConfig?.bundles?.vendorShared && typeof this.bundleConfig.bundles.vendorShared === 'object') {
264
- return this.bundleConfig.bundles.vendorShared;
265
- }
266
-
267
- return this.findBundleEntryByName(this.bundleConfig?.bundles?.shared, 'vendor-shared');
268
- }
269
-
270
- isVendorSharedAlias(bundleName) {
271
- if (typeof bundleName !== 'string') {
272
- return false;
273
- }
274
-
275
- const normalized = bundleName.toLowerCase();
276
- return normalized === 'vendor-shared' || normalized === 'vendorshared';
277
- }
278
-
279
- registerVendorSharedDependencies(bundleModule, metadata, bundleName, registerResult) {
280
- const isVendorShared = this.isVendorSharedBundleName(metadata?.bundleKey)
281
- || this.isVendorSharedBundleName(bundleName)
282
- || metadata?.registerVendorSharedDependencies === true;
283
-
284
- if (!isVendorShared) {
285
- return;
286
- }
287
-
288
- let sharedDeps = bundleModule?.SLICE_SHARED_DEPS;
289
- if (!sharedDeps && registerResult && typeof registerResult === 'object') {
290
- sharedDeps = registerResult.SLICE_SHARED_DEPS
291
- || registerResult.SLICE_BUNDLE_DEPENDENCIES
292
- || registerResult;
293
- }
294
-
295
- if (!sharedDeps || typeof sharedDeps !== 'object') {
296
- return;
297
- }
298
-
299
- if (!window.__SLICE_SHARED_DEPS__ || typeof window.__SLICE_SHARED_DEPS__ !== 'object') {
300
- window.__SLICE_SHARED_DEPS__ = {};
301
- }
302
-
303
- Object.assign(window.__SLICE_SHARED_DEPS__, sharedDeps);
304
- }
305
-
306
- isVendorSharedBundleName(bundleName) {
307
- return typeof bundleName === 'string' && bundleName.toLowerCase() === 'vendor-shared';
308
- }
309
-
310
- /**
311
- * 📦 Registers a bundle's components (called automatically by bundle files)
312
- */
313
- registerBundleLegacy(bundle) {
314
- const { components, metadata } = bundle;
315
-
316
- slice.logger.logInfo('Controller', `📦 Registering bundle: ${metadata.type} (${metadata.componentCount} components)`);
317
-
318
- // Phase 1: Register templates and CSS for all components first
319
- for (const [componentName, componentData] of Object.entries(components)) {
320
- try {
321
- // Register HTML template
322
- if (componentData.html !== undefined && !this.templates.has(componentName)) {
323
- const template = document.createElement('template');
324
- template.innerHTML = componentData.html || '';
325
- this.templates.set(componentName, template);
326
- }
327
-
328
- // Register CSS styles
329
- if (componentData.css !== undefined && !this.requestedStyles.has(componentName)) {
330
- // Use the existing stylesManager to register component styles
331
- if (window.slice && window.slice.stylesManager) {
332
- window.slice.stylesManager.registerComponentStyles(componentName, componentData.css || '');
333
- this.requestedStyles.add(componentName);
334
- }
335
- }
336
- } catch (error) {
337
- slice.logger.logError('Controller', `❌ Failed to register assets for ${componentName}`, error);
338
- }
339
- }
340
-
341
- // Phase 2: Evaluate all external file dependencies
342
- const processedDeps = new Set();
343
- for (const [componentName, componentData] of Object.entries(components)) {
344
- if (componentData.dependencies) {
345
- for (const [depName, depContent] of Object.entries(componentData.dependencies)) {
346
- if (!processedDeps.has(depName)) {
347
- try {
348
- // Convert ES6 exports to global assignments
349
- let processedContent = depContent
350
- .replace(/export\s+const\s+(\w+)\s*=/g, 'window.$1 =')
351
- .replace(/export\s+let\s+(\w+)\s*=/g, 'window.$1 =')
352
- .replace(/export\s+var\s+(\w+)\s*=/g, 'window.$1 =')
353
- .replace(/export\s+function\s+(\w+)/g, 'window.$1 = function')
354
- .replace(/export\s+default\s+/g, 'window.defaultExport =')
355
- .replace(/export\s*{\s*([^}]+)\s*}/g, (match, exports) => {
356
- return exports
357
- .split(',')
358
- .map((exp) => {
359
- const cleanExp = exp.trim();
360
- const varName = cleanExp.split(' as ')[0].trim();
361
- return `window.${varName} = ${varName};`;
362
- })
363
- .join('\n');
364
- })
365
- // Remove any remaining export keywords
366
- .replace(/^\s*export\s+/gm, '');
367
-
368
- // Evaluate the dependency
369
- try {
370
- new Function('slice', 'customElements', 'window', 'document', processedContent)(
371
- window.slice,
372
- window.customElements,
373
- window,
374
- window.document
375
- );
376
- } catch (evalError) {
377
- slice.logger.logWarning('Controller', `❌ Failed to evaluate processed dependency ${depName}: ${evalError}`);
378
- slice.logger.logWarning('Controller', `Processed content preview: ${processedContent.substring(0, 200)}`);
379
- // Try evaluating the original content as fallback
380
- try {
381
- new Function('slice', 'customElements', 'window', 'document', depContent)(
382
- window.slice,
383
- window.customElements,
384
- window,
385
- window.document
386
- );
387
- slice.logger.logInfo('Controller', `✅ Fallback evaluation succeeded for ${depName}`);
388
- } catch (fallbackError) {
389
- slice.logger.logWarning('Controller', `❌ Fallback evaluation also failed for ${depName}: ${fallbackError}`);
390
- }
391
- }
392
-
393
- processedDeps.add(depName);
394
- slice.logger.logInfo('Controller', `📄 Dependency loaded: ${depName}`);
395
- } catch (depError) {
396
- slice.logger.logWarning('Controller', `⚠️ Failed to load dependency ${depName} for ${componentName}: ${depError}`);
397
- }
398
- }
399
- }
400
- }
401
- }
402
-
403
- // Phase 3: Evaluate all component classes (now that dependencies are available)
404
- for (const [componentName, componentData] of Object.entries(components)) {
405
- // For JavaScript classes, we need to evaluate the code
406
- if (componentData.js && !this.classes.has(componentName)) {
407
- try {
408
- // Create evaluation context with dependencies
409
- let evalCode = componentData.js;
410
-
411
- // Prepend dependencies to make them available
412
- if (componentData.dependencies) {
413
- const depCode = Object.entries(componentData.dependencies)
414
- .map(([depName, depContent]) => {
415
- // Convert ES6 exports to global assignments
416
- return depContent
417
- .replace(/export\s+const\s+(\w+)\s*=/g, 'window.$1 =')
418
- .replace(/export\s+let\s+(\w+)\s*=/g, 'window.$1 =')
419
- .replace(/export\s+function\s+(\w+)/g, 'window.$1 = function')
420
- .replace(/export\s+default\s+/g, 'window.defaultExport =')
421
- .replace(/export\s*{\s*([^}]+)\s*}/g, (match, exports) => {
422
- return exports
423
- .split(',')
424
- .map((exp) => {
425
- const cleanExp = exp.trim();
426
- return `window.${cleanExp} = ${cleanExp};`;
427
- })
428
- .join('\n');
429
- });
430
- })
431
- .join('\n\n');
432
-
433
- evalCode = depCode + '\n\n' + evalCode;
434
- }
435
-
436
- // Evaluate the complete code
437
- const componentClass = new Function(
438
- 'slice',
439
- 'customElements',
440
- 'window',
441
- 'document',
442
- `
443
- "use strict";
444
- ${evalCode}
445
- return ${componentName};
446
- `
447
- )(window.slice, window.customElements, window, window.document);
448
-
449
- if (componentClass) {
450
- this.classes.set(componentName, componentClass);
451
- slice.logger.logInfo('Controller', `📝 Class registered for: ${componentName}`);
452
- }
453
- } catch (error) {
454
- slice.logger.logWarning('Controller', `❌ Failed to evaluate class for ${componentName}: ${error}`);
455
- slice.logger.logWarning('Controller', `Code that failed: ${componentData.js.substring(0, 200) + '...'}`);
456
- }
457
- }
458
- }
459
- }
460
-
461
- /**
462
- * 📦 New bundle registration method (simplified and robust)
463
- */
464
- registerBundle(bundle) {
465
- const validation = this.validateBundle(bundle);
466
- if (!validation.isValid) {
467
- slice.logger.logWarning('Controller', `❌ Bundle validation failed: ${validation.error}`);
468
- return Promise.resolve(false);
469
- }
470
-
471
- // Set tracking flags synchronously before any async work, so callers that
472
- // await import() see the flags set immediately when the Promise resolves.
473
- const { components, metadata } = bundle;
474
- const bundleKey = metadata?.bundleKey;
475
- if (bundleKey) {
476
- this.loadedBundles.add(bundleKey);
477
- if (metadata?.type === 'critical') {
478
- this.criticalBundleLoaded = true;
479
- }
480
- }
481
-
482
- slice.logger.logInfo('Controller', `📦 Registering bundle: ${metadata.type} (${metadata.componentCount} components)`);
483
-
484
- const entries = Object.entries(components);
485
- const chunkSize = 50;
486
- let index = 0;
487
-
488
- return new Promise((resolve) => {
489
- const processChunk = () => {
490
- const sliceEntries = entries.slice(index, index + chunkSize);
491
-
492
- for (const [componentName, componentData] of sliceEntries) {
493
- try {
494
- if (componentData.html !== undefined && !this.templates.has(componentName)) {
495
- const template = document.createElement('template');
496
- template.innerHTML = componentData.html || '';
497
- this.templates.set(componentName, template);
498
- }
499
-
500
- if (componentData.css !== undefined && !this.requestedStyles.has(componentName)) {
501
- if (window.slice && window.slice.stylesManager) {
502
- window.slice.stylesManager.registerComponentStyles(componentName, componentData.css || '');
503
- this.requestedStyles.add(componentName);
504
- }
505
- }
506
-
507
- if (componentData.class && !this.classes.has(componentName)) {
508
- const registeredName = componentData.isFramework
509
- ? `Framework/Structural/${componentName}`
510
- : componentName;
511
- this.classes.set(registeredName, componentData.class);
512
- if (componentName === 'Loading') {
513
- slice.logger.logInfo(
514
- 'Controller',
515
- `🔎 Bundle class registered: Loading (registeredName=${registeredName}, type=${typeof componentData.class}, isFunction=${typeof componentData.class === 'function'})`
516
- );
517
- }
518
- if (componentName === 'InputSearchDocs' || componentName === 'MainMenu') {
519
- slice.logger.logInfo(
520
- 'Controller',
521
- `🔎 Bundle class registered: ${componentName} (registeredName=${registeredName}, type=${typeof componentData.class}, isFunction=${typeof componentData.class === 'function'})`
522
- );
523
- }
524
- }
525
- } catch (error) {
526
- slice.logger.logError('Controller', `❌ Failed to register component ${componentName}`, error);
527
- }
528
- }
529
-
530
- index += chunkSize;
531
- if (index < entries.length) {
532
- if (typeof requestIdleCallback === 'function') {
533
- requestIdleCallback(processChunk);
534
- } else {
535
- setTimeout(processChunk, 0);
536
- }
537
- return;
538
- }
539
-
540
- slice.logger.logInfo('Controller', `✅ Bundle registration completed: ${metadata.componentCount} components processed`);
541
- resolve(true);
542
- };
543
-
544
- processChunk();
545
- });
546
- }
547
-
548
- /**
549
- * Validates bundle structure before registering.
550
- * @param {object} bundle
551
- * @returns {{isValid: boolean, error?: string}}
552
- */
553
- validateBundle(bundle) {
554
- if (!bundle || typeof bundle !== 'object') {
555
- return { isValid: false, error: 'Bundle payload is invalid' };
556
- }
557
-
558
- if (!bundle.metadata || typeof bundle.metadata !== 'object') {
559
- return { isValid: false, error: 'Bundle metadata missing' };
560
- }
561
-
562
- if (!bundle.components || typeof bundle.components !== 'object') {
563
- return { isValid: false, error: 'Bundle components missing' };
564
- }
565
-
566
- if (typeof bundle.metadata.componentCount !== 'number') {
567
- return { isValid: false, error: 'Bundle metadata missing componentCount' };
568
- }
569
-
570
- if (bundle.metadata.componentCount !== Object.keys(bundle.components).length) {
571
- return { isValid: false, error: 'Bundle component count mismatch' };
572
- }
573
-
574
- const maxComponents = 5000;
575
- if (bundle.metadata.componentCount > maxComponents) {
576
- return { isValid: false, error: 'Bundle component count exceeds limit' };
577
- }
578
-
579
- return { isValid: true };
580
- }
581
-
582
- /**
583
- * 📦 Determines which bundle to load for a component
584
- */
585
- getBundleForComponent(componentName) {
586
- if (!this.bundleConfig?.bundles) return null;
587
-
588
- // Check if component is in critical bundle
589
- if (this.bundleConfig.bundles.critical?.components?.includes(componentName)) {
590
- return 'critical';
591
- }
592
-
593
- // Find component in route bundles
594
- if (this.bundleConfig.bundles.routes) {
595
- for (const [bundleName, bundleInfo] of Object.entries(this.bundleConfig.bundles.routes)) {
596
- if (bundleInfo.components?.includes(componentName)) {
597
- return bundleName;
598
- }
599
- }
600
- }
601
-
602
- return null;
603
- }
604
-
605
- /**
606
- * 📦 Checks if a component is available from loaded bundles
607
- */
608
- isComponentFromBundle(componentName) {
609
- if (!this.bundleConfig?.bundles) return false;
610
-
611
- // Check critical bundle
612
- if (this.bundleConfig.bundles.critical?.components?.includes(componentName)) {
613
- return this.criticalBundleLoaded;
614
- }
615
-
616
- // Check route bundles
617
- if (this.bundleConfig.bundles.routes) {
618
- for (const [bundleName, bundleInfo] of Object.entries(this.bundleConfig.bundles.routes)) {
619
- if (bundleInfo.components?.includes(componentName)) {
620
- return this.loadedBundles.has(bundleName);
621
- }
622
- }
623
- }
624
-
625
- return false;
626
- }
627
-
628
- /**
629
- * 📦 Gets component data from loaded bundles
630
- */
631
- getComponentFromBundle(componentName) {
632
- if (!this.bundleConfig?.bundles) return null;
633
-
634
- // Find component in any loaded bundle
635
- const allBundles = [
636
- { name: 'critical', data: this.bundleConfig.bundles.critical },
637
- ...Object.entries(this.bundleConfig.bundles.routes || {}).map(([name, data]) => ({ name, data })),
638
- ];
639
-
640
- for (const { name: bundleName, data: bundleData } of allBundles) {
641
- if (bundleData?.components?.includes(componentName) && this.loadedBundles.has(bundleName)) {
642
- // Find the bundle file and extract component data
643
- // This is a simplified version - in practice you'd need to access the loaded bundle data
644
- return { bundleName, componentName };
645
- }
646
- }
647
-
648
- return null;
649
- }
650
-
651
- logActiveComponents() {
652
- this.activeComponents.forEach((component) => {
653
- let parent = component.parentComponent;
654
- let parentName = parent ? parent.constructor.name : null;
655
- });
656
- }
657
-
658
- getTopParentsLinkedToActiveComponents() {
659
- let topParentsLinkedToActiveComponents = new Map();
660
- this.activeComponents.forEach((component) => {
661
- let parent = component.parentComponent;
662
- while (parent && parent.parentComponent) {
663
- parent = parent.parentComponent;
664
- }
665
- if (!topParentsLinkedToActiveComponents.has(parent)) {
666
- topParentsLinkedToActiveComponents.set(parent, []);
667
- }
668
- topParentsLinkedToActiveComponents.get(parent).push(component);
669
- });
670
- return topParentsLinkedToActiveComponents;
671
- }
672
-
673
- verifyComponentIds(component) {
674
- const htmlId = component.id;
675
-
676
- if (htmlId && htmlId.trim() !== '') {
677
- if (this.activeComponents.has(htmlId)) {
678
- slice.logger.logError(
679
- 'Controller',
680
- `A component with the same html id attribute is already registered: ${htmlId}`
681
- );
682
- return false;
683
- }
684
- }
685
-
686
- let sliceId = component.sliceId;
687
-
688
- if (sliceId && sliceId.trim() !== '') {
689
- if (this.activeComponents.has(sliceId)) {
690
- slice.logger.logError(
691
- 'Controller',
692
- `A component with the same slice id attribute is already registered: ${sliceId}`
693
- );
694
- return false;
695
- }
696
- } else {
697
- sliceId = `${component.constructor.name[0].toLowerCase()}${component.constructor.name.slice(1)}-${this.idCounter}`;
698
- component.sliceId = sliceId;
699
- this.idCounter++;
700
- }
701
-
702
- component.sliceId = sliceId;
703
- return true;
704
- }
705
-
706
- /**
707
- * Registra un componente y actualiza el índice de relaciones padre-hijo
708
- * 🚀 OPTIMIZADO: Ahora mantiene childrenIndex y precalcula profundidad
709
- */
710
- registerComponent(component, parent = null) {
711
- component.parentComponent = parent;
712
-
713
- // 🚀 OPTIMIZACIÓN: Precalcular y guardar profundidad
714
- component._depth = parent ? (parent._depth || 0) + 1 : 0;
715
-
716
- // Registrar en activeComponents
717
- this.activeComponents.set(component.sliceId, component);
718
-
719
- // Exponer sliceId como atributo HTML para búsqueda por DOM (destroyByContainer, etc.)
720
- if (typeof component.setAttribute === 'function') {
721
- component.setAttribute('slice-id', component.sliceId);
722
- }
723
-
724
- // 🚀 OPTIMIZACIÓN: Actualizar índice inverso de hijos
725
- if (parent) {
726
- if (!this.childrenIndex.has(parent.sliceId)) {
727
- this.childrenIndex.set(parent.sliceId, new Set());
728
- }
729
- this.childrenIndex.get(parent.sliceId).add(component.sliceId);
730
- }
731
-
732
- return true;
733
- }
734
-
735
- registerComponentsRecursively(component, parent = null) {
736
- // Assign parent if not already set
737
- if (!component.parentComponent) {
738
- component.parentComponent = parent;
739
- }
740
-
741
- // Recursively assign parent to children
742
- component.querySelectorAll('*').forEach((child) => {
743
- if (child.tagName.startsWith('SLICE-')) {
744
- // Only the call that establishes the DIRECT parent link feeds the
745
- // index the depth-first recursion sets a node's parent before an
746
- // outer ancestor's forEach reaches it, so `component` here is the
747
- // immediate enclosing component.
748
- if (!child.parentComponent) {
749
- child.parentComponent = component;
750
- // 🔁 Maintain childrenIndex so destroyComponent(parent) cascades
751
- // to children built via slice.build (which registers them WITHOUT
752
- // a parent, leaving the index otherwise empty). Without this, a
753
- // parent's destroy never finds — and never cleans up — its children.
754
- if (child.sliceId && component.sliceId) {
755
- if (!this.childrenIndex.has(component.sliceId)) {
756
- this.childrenIndex.set(component.sliceId, new Set());
757
- }
758
- this.childrenIndex.get(component.sliceId).add(child.sliceId);
759
- child._depth = (component._depth || 0) + 1;
760
- }
761
- }
762
- this.registerComponentsRecursively(child, component);
763
- }
764
- });
765
- }
766
-
767
- /**
768
- * Get a registered component by sliceId.
769
- * @param {string} sliceId
770
- * @returns {HTMLElement|undefined}
771
- */
772
- getComponent(sliceId) {
773
- return this.activeComponents.get(sliceId);
774
- }
775
-
776
- /**
777
- * Get-or-create a single instance keyed by sliceId, deduplicating concurrent
778
- * builds. Returns the existing instance if already registered, the in-flight
779
- * promise if a build is underway, or otherwise memoizes a fresh build via
780
- * `builder` (an injected closure the controller never builds by itself).
781
- *
782
- * The in-flight promise is removed once it settles, so a failed build (which
783
- * resolves to `null` and never registers in activeComponents) can be retried
784
- * by a later call and never poisons the registry.
785
- *
786
- * @param {string} sliceId
787
- * @param {() => Promise<any>} builder
788
- * @returns {any|Promise<any>} instance (sync) or Promise<instance>
789
- */
790
- getOrCreate(sliceId, builder) {
791
- const existing = this.activeComponents.get(sliceId);
792
- if (existing) return existing;
793
-
794
- const pending = this._pendingBuilds.get(sliceId);
795
- if (pending) return pending;
796
-
797
- const promise = Promise.resolve(builder())
798
- .finally(() => this._pendingBuilds.delete(sliceId));
799
- this._pendingBuilds.set(sliceId, promise);
800
- return promise;
801
- }
802
-
803
- loadTemplateToComponent(component) {
804
- const className = component.constructor.name;
805
- const template = this.templates.get(className);
806
-
807
- if (!template) {
808
- slice.logger.logError(`Template not found for component: ${className}`);
809
- return;
810
- }
811
-
812
- component.innerHTML = template.innerHTML;
813
- return component;
814
- }
815
-
816
- getComponentCategory(componentSliceId) {
817
- return this.componentCategories.get(componentSliceId);
818
- }
819
-
820
- /**
821
- * Fetch component resources (html, css, styles, theme).
822
- * @param {string} componentName
823
- * @param {'html'|'css'|'theme'|'styles'} resourceType
824
- * @param {string} [componentCategory]
825
- * @param {string} [customPath]
826
- * @returns {Promise<string>}
827
- */
828
- async fetchText(componentName, resourceType, componentCategory, customPath) {
829
- try {
830
- const baseUrl = window.location.origin;
831
- let path;
832
-
833
- if (!componentCategory) {
834
- componentCategory = this.componentCategories.get(componentName);
835
- }
836
-
837
- let isVisual = resourceType === 'html' || resourceType === 'css';
838
-
839
- if (isVisual) {
840
- if (slice.paths.components[componentCategory]) {
841
- path = `${baseUrl}${slice.paths.components[componentCategory].path}/${componentName}`;
842
- resourceType === 'html' ? (path += `/${componentName}.html`) : (path += `/${componentName}.css`);
843
- } else {
844
- if (componentCategory === 'Structural') {
845
- path = `${baseUrl}/Slice/Components/Structural/${componentName}`;
846
- resourceType === 'html' ? (path += `/${componentName}.html`) : (path += `/${componentName}.css`);
847
- } else {
848
- throw new Error(`Component category '${componentCategory}' not found in paths configuration`);
849
- }
850
- }
851
- }
852
-
853
- if (resourceType === 'theme') {
854
- path = `${baseUrl}${slice.paths.themes}/${componentName}.css`;
855
- }
856
-
857
- if (resourceType === 'styles') {
858
- path = `${baseUrl}${slice.paths.styles}/${componentName}.css`;
859
- }
860
-
861
- if (customPath) {
862
- path = customPath;
863
- }
864
-
865
- slice.logger.logInfo('Controller', `Fetching ${resourceType} from: ${path}`);
866
-
867
- const response = await fetch(path);
868
-
869
- if (!response.ok) {
870
- throw new Error(`Failed to fetch ${path}: ${response.statusText}`);
871
- }
872
-
873
- const content = await response.text();
874
- slice.logger.logInfo('Controller', `Successfully fetched ${resourceType} for ${componentName}`);
875
- return content;
876
- } catch (error) {
877
- slice.logger.logError('Controller', `Error fetching ${resourceType} for component ${componentName}:`, error);
878
- throw error;
879
- }
880
- }
881
-
882
- /**
883
- * Apply props to a component using static defaults and setters.
884
- * @param {HTMLElement} component
885
- * @param {Object} props
886
- * @returns {void}
887
- */
888
- setComponentProps(component, props) {
889
- const ComponentClass = component.constructor;
890
- const componentName = ComponentClass.name;
891
-
892
- // Aplicar defaults si tiene static props
893
- if (ComponentClass.props) {
894
- this.applyDefaultProps(component, ComponentClass.props, props);
895
- }
896
-
897
- // Validar solo en desarrollo
898
- if (ComponentClass.props && !slice.isProduction()) {
899
- this.validatePropsInDevelopment(ComponentClass, props, componentName);
900
- }
901
-
902
- // Aplicar props
903
- for (const prop in props) {
904
- component[`_${prop}`] = null;
905
- component[prop] = props[prop];
906
- }
907
- }
908
-
909
- getComponentPropsForDebugger(component) {
910
- const ComponentClass = component.constructor;
911
-
912
- if (ComponentClass.props) {
913
- return {
914
- availableProps: Object.keys(ComponentClass.props),
915
- propsConfig: ComponentClass.props,
916
- usedProps: this.extractUsedProps(component, ComponentClass.props),
917
- };
918
- } else {
919
- return {
920
- availableProps: this.extractUsedProps(component),
921
- propsConfig: null,
922
- usedProps: this.extractUsedProps(component),
923
- };
924
- }
925
- }
926
-
927
- applyDefaultProps(component, staticProps, providedProps) {
928
- Object.entries(staticProps).forEach(([prop, config]) => {
929
- if (config.default !== undefined && !(prop in (providedProps || {}))) {
930
- component[`_${prop}`] = null;
931
- component[prop] = config.default;
932
- }
933
- });
934
- }
935
-
936
- validatePropsInDevelopment(ComponentClass, providedProps, componentName) {
937
- const staticProps = ComponentClass.props;
938
- const usedProps = Object.keys(providedProps || {});
939
-
940
- const availableProps = Object.keys(staticProps);
941
- const unknownProps = usedProps.filter((prop) => !availableProps.includes(prop));
942
-
943
- if (unknownProps.length > 0) {
944
- slice.logger.logWarning(
945
- 'PropsValidator',
946
- `${componentName}: Unknown props [${unknownProps.join(', ')}]. Available: [${availableProps.join(', ')}]`
947
- );
948
- }
949
-
950
- const requiredProps = Object.entries(staticProps)
951
- .filter(([_, config]) => config.required)
952
- .map(([prop, _]) => prop);
953
-
954
- const missingRequired = requiredProps.filter((prop) => !(prop in (providedProps || {})));
955
- if (missingRequired.length > 0) {
956
- slice.logger.logError(componentName, `Missing required props: [${missingRequired.join(', ')}]`);
957
- }
958
-
959
- const invalidAllowedValueProps = collectInvalidAllowedValueProps(staticProps, providedProps);
960
- invalidAllowedValueProps.forEach(({ propName, value, allowedValues }) => {
961
- slice.logger.logError(
962
- componentName,
963
- `Invalid value for prop "${propName}": ${JSON.stringify(value)}. Allowed values: ${formatAllowedValuesForLog(allowedValues)}`
964
- );
965
- });
966
- }
967
-
968
- extractUsedProps(component, staticProps = null) {
969
- const usedProps = {};
970
-
971
- if (staticProps) {
972
- Object.keys(staticProps).forEach((prop) => {
973
- if (component[prop] !== undefined) {
974
- usedProps[prop] = component[prop];
975
- }
976
- });
977
- } else {
978
- Object.getOwnPropertyNames(component).forEach((key) => {
979
- if (key.startsWith('_') && key !== '_isActive') {
980
- const propName = key.substring(1);
981
- usedProps[propName] = component[propName];
982
- }
983
- });
984
- }
985
-
986
- return usedProps;
987
- }
988
-
989
- // ============================================================================
990
- // 🚀 MÉTODOS DE DESTRUCCIÓN OPTIMIZADOS
991
- // ============================================================================
992
-
993
- /**
994
- * Encuentra recursivamente todos los hijos de un componente
995
- * 🚀 OPTIMIZADO: O(m) en lugar de O(n*d) - usa childrenIndex
996
- * @param {string} parentSliceId - sliceId del componente padre
997
- * @param {Set<string>} collected - Set de sliceIds ya recolectados
998
- * @returns {Set<string>} Set de todos los sliceIds de componentes hijos
999
- */
1000
- findAllChildComponents(parentSliceId, collected = new Set()) {
1001
- // 🚀 Buscar directamente en el índice: O(1)
1002
- const children = this.childrenIndex.get(parentSliceId);
1003
-
1004
- if (!children) return collected;
1005
-
1006
- // 🚀 Iterar solo los hijos directos: O(k) donde k = número de hijos
1007
- for (const childSliceId of children) {
1008
- collected.add(childSliceId);
1009
- // Recursión solo sobre hijos, no todos los componentes
1010
- this.findAllChildComponents(childSliceId, collected);
1011
- }
1012
-
1013
- return collected;
1014
- }
1015
-
1016
- /**
1017
- * Encuentra recursivamente todos los componentes dentro de un contenedor DOM
1018
- * Útil para destroyByContainer cuando no tenemos el sliceId del padre
1019
- * @param {HTMLElement} container - Elemento contenedor
1020
- * @param {Set<string>} collected - Set de sliceIds ya recolectados
1021
- * @returns {Set<string>} Set de todos los sliceIds encontrados
1022
- */
1023
- findAllNestedComponentsInContainer(container, collected = new Set()) {
1024
- // Buscar todos los elementos con slice-id en el contenedor
1025
- const sliceComponents = container.querySelectorAll('[slice-id]');
1026
-
1027
- sliceComponents.forEach((element) => {
1028
- const sliceId = element.getAttribute('slice-id') || element.sliceId;
1029
- if (sliceId && this.activeComponents.has(sliceId)) {
1030
- collected.add(sliceId);
1031
- // 🚀 Usar índice para buscar hijos recursivamente
1032
- this.findAllChildComponents(sliceId, collected);
1033
- }
1034
- });
1035
-
1036
- return collected;
1037
- }
1038
-
1039
- /**
1040
- * Destruye uno o múltiples componentes DE FORMA RECURSIVA
1041
- * 🚀 OPTIMIZADO: O(m log m) en lugar de O(n*d + m log m)
1042
- * @param {HTMLElement|Array<HTMLElement>|string|Array<string>} components
1043
- * @returns {number} Cantidad de componentes destruidos (incluyendo hijos)
1044
- */
1045
- destroyComponent(components) {
1046
- const toDestroy = Array.isArray(components) ? components : [components];
1047
- const allSliceIdsToDestroy = new Set();
1048
-
1049
- // PASO 1: Recolectar todos los componentes padres y sus hijos recursivamente
1050
- for (const item of toDestroy) {
1051
- let sliceId = null;
1052
-
1053
- if (typeof item === 'string') {
1054
- if (!this.activeComponents.has(item)) {
1055
- slice.logger.logWarning('Controller', `Component with sliceId "${item}" not found`);
1056
- continue;
1057
- }
1058
- sliceId = item;
1059
- } else if (item && item.sliceId) {
1060
- sliceId = item.sliceId;
1061
- } else {
1062
- slice.logger.logWarning('Controller', `Invalid component or sliceId provided to destroyComponent`);
1063
- continue;
1064
- }
1065
-
1066
- allSliceIdsToDestroy.add(sliceId);
1067
-
1068
- // 🚀 OPTIMIZADO: Usa childrenIndex en lugar de recorrer todos los componentes
1069
- this.findAllChildComponents(sliceId, allSliceIdsToDestroy);
1070
- }
1071
-
1072
- // PASO 2: Ordenar por profundidad (más profundos primero)
1073
- // 🚀 OPTIMIZADO: Usa _depth precalculada en lugar de calcularla cada vez
1074
- const sortedSliceIds = Array.from(allSliceIdsToDestroy).sort((a, b) => {
1075
- const compA = this.activeComponents.get(a);
1076
- const compB = this.activeComponents.get(b);
1077
-
1078
- if (!compA || !compB) return 0;
1079
-
1080
- // 🚀 O(1) en lugar de O(d) - usa profundidad precalculada
1081
- return (compB._depth || 0) - (compA._depth || 0);
1082
- });
1083
-
1084
- let destroyedCount = 0;
1085
-
1086
- // PASO 3: Destruir en orden correcto (hijos antes que padres)
1087
- for (const sliceId of sortedSliceIds) {
1088
- const component = this.activeComponents.get(sliceId);
1089
-
1090
- if (!component) continue;
1091
-
1092
- // Ejecutar hook beforeDestroy si existe
1093
- if (typeof component.beforeDestroy === 'function') {
1094
- try {
1095
- component.beforeDestroy();
1096
- } catch (error) {
1097
- slice.logger.logError('Controller', `Error in beforeDestroy for ${sliceId}`, error);
1098
- }
1099
- }
1100
-
1101
- // Limpiar suscripciones de eventos del componente
1102
- if (slice.events) {
1103
- slice.events.cleanupComponent(sliceId);
1104
- }
1105
-
1106
- // 🚀 Limpiar del índice de hijos
1107
- this.childrenIndex.delete(sliceId);
1108
-
1109
- // Si tiene padre, remover de la lista de hijos del padre
1110
- if (component.parentComponent) {
1111
- const parentChildren = this.childrenIndex.get(component.parentComponent.sliceId);
1112
- if (parentChildren) {
1113
- parentChildren.delete(sliceId);
1114
- // Si el padre no tiene más hijos, eliminar entrada vacía
1115
- if (parentChildren.size === 0) {
1116
- this.childrenIndex.delete(component.parentComponent.sliceId);
1117
- }
1118
- }
1119
- }
1120
-
1121
- // Eliminar del mapa de componentes activos
1122
- this.activeComponents.delete(sliceId);
1123
-
1124
- // Remover del DOM si está conectado
1125
- if (component.isConnected) {
1126
- component.remove();
1127
- }
1128
-
1129
- destroyedCount++;
1130
- }
1131
-
1132
- if (destroyedCount > 0) {
1133
- slice.logger.logInfo('Controller', `Destroyed ${destroyedCount} component(s) recursively`);
1134
- }
1135
-
1136
- return destroyedCount;
1137
- }
1138
-
1139
- /**
1140
- * Destruye todos los componentes Slice dentro de un contenedor (RECURSIVO)
1141
- * 🚀 OPTIMIZADO: Usa el índice inverso para búsqueda rápida
1142
- * @param {HTMLElement} container - Elemento contenedor
1143
- * @returns {number} Cantidad de componentes destruidos
1144
- */
1145
- destroyByContainer(container) {
1146
- if (!container) {
1147
- slice.logger.logWarning('Controller', 'No container provided to destroyByContainer');
1148
- return 0;
1149
- }
1150
-
1151
- // 🚀 Recolectar componentes usando índice optimizado
1152
- const allSliceIds = this.findAllNestedComponentsInContainer(container);
1153
-
1154
- if (allSliceIds.size === 0) {
1155
- return 0;
1156
- }
1157
-
1158
- // Destruir usando el método principal optimizado
1159
- const count = this.destroyComponent(Array.from(allSliceIds));
1160
-
1161
- if (count > 0) {
1162
- slice.logger.logInfo('Controller', `Destroyed ${count} component(s) from container (including nested)`);
1163
- }
1164
-
1165
- return count;
1166
- }
1167
-
1168
- /**
1169
- * Destruye componentes cuyos sliceId coincidan con un patrón (RECURSIVO)
1170
- * 🚀 OPTIMIZADO: Usa destrucción optimizada
1171
- * @param {string|RegExp} pattern - Patrón a buscar
1172
- * @returns {number} Cantidad de componentes destruidos
1173
- */
1174
- destroyByPattern(pattern) {
1175
- const componentsToDestroy = [];
1176
- const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
1177
-
1178
- for (const [sliceId, component] of this.activeComponents) {
1179
- if (regex.test(sliceId)) {
1180
- componentsToDestroy.push(component);
1181
- }
1182
- }
1183
-
1184
- if (componentsToDestroy.length === 0) {
1185
- return 0;
1186
- }
1187
-
1188
- const count = this.destroyComponent(componentsToDestroy);
1189
-
1190
- if (count > 0) {
1191
- slice.logger.logInfo(
1192
- 'Controller',
1193
- `Destroyed ${count} component(s) matching pattern: ${pattern} (including nested)`
1194
- );
1195
- }
1196
-
1197
- return count;
1198
- }
1199
- }
1
+ import components from '/Components/components.js';
2
+ import { collectInvalidAllowedValueProps, formatAllowedValuesForLog } from './allowedValuesValidation.js';
3
+
4
+ export default class Controller {
5
+ constructor() {
6
+ this.componentCategories = new Map(Object.entries(components));
7
+ this.templates = new Map();
8
+ this.classes = new Map();
9
+ this.requestedStyles = new Set(); // ✅ CRÍTICO: Para tracking de CSS cargados
10
+ this.activeComponents = new Map();
11
+
12
+ // 🚀 OPTIMIZACIÓN: Índice inverso para búsqueda rápida de hijos
13
+ // parentSliceId → Set<childSliceId>
14
+ this.childrenIndex = new Map();
15
+
16
+ // 📦 Bundle system
17
+ this.loadedBundles = new Set();
18
+ this.bundleConfig = null;
19
+ this.criticalBundleLoaded = false;
20
+ this.bundleImportPromises = new Map();
21
+ this.bundleLoadPromises = new Map();
22
+
23
+ // 🔁 Singleton builds in flight: sliceId → Promise<instance>.
24
+ // Lets concurrent build({singleton:true}) calls share one build instead
25
+ // of racing on a duplicate sliceId. Entries are transient (deleted on settle).
26
+ this._pendingBuilds = new Map();
27
+
28
+ this.idCounter = 0;
29
+ }
30
+
31
+ /**
32
+ * 📦 Initializes bundle system (called automatically when config is loaded)
33
+ */
34
+ initializeBundles(config = null) {
35
+ if (config) {
36
+ this.bundleConfig = config;
37
+
38
+ // Register critical bundle components if available
39
+ if (config.bundles?.critical) {
40
+ // Critical bundle will be loaded explicitly
41
+ }
42
+ this.criticalBundleLoaded = false;
43
+ } else {
44
+ // No bundles available, will use individual component loading
45
+ this.bundleConfig = null;
46
+ this.criticalBundleLoaded = false;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Import a bundle URL once per page session.
52
+ * Reuses the same Promise for concurrent callers.
53
+ * @param {string} bundlePath
54
+ * @returns {Promise<any>}
55
+ */
56
+ importBundleOnce(bundlePath) {
57
+ if (!bundlePath) {
58
+ return Promise.reject(new Error('Bundle path is required'));
59
+ }
60
+
61
+ if (this.bundleImportPromises.has(bundlePath)) {
62
+ return this.bundleImportPromises.get(bundlePath);
63
+ }
64
+
65
+ const importPromise = import(bundlePath).catch((error) => {
66
+ this.bundleImportPromises.delete(bundlePath);
67
+ throw error;
68
+ });
69
+
70
+ this.bundleImportPromises.set(bundlePath, importPromise);
71
+ return importPromise;
72
+ }
73
+
74
+ buildBundleImportPath(bundleInfo) {
75
+ if (!bundleInfo || typeof bundleInfo.file !== 'string' || bundleInfo.file.length === 0) {
76
+ throw new Error('Bundle file is required');
77
+ }
78
+
79
+ const basePath = `/bundles/${bundleInfo.file}`;
80
+ const bundleHash = typeof bundleInfo.hash === 'string' ? bundleInfo.hash.trim() : '';
81
+ if (!bundleHash) {
82
+ return basePath;
83
+ }
84
+
85
+ return `${basePath}?v=${encodeURIComponent(bundleHash)}`;
86
+ }
87
+
88
+ /**
89
+ * Validate Bundling V2 module contract.
90
+ * Requires named exports: SLICE_BUNDLE_META and registerAll.
91
+ * @param {any} bundleModule
92
+ * @param {string} [bundleName]
93
+ * @returns {{metadata: object, registerAll: Function}}
94
+ */
95
+ async validateBundleModule(bundleModule, bundleName = 'unknown') {
96
+ const metadata = bundleModule?.SLICE_BUNDLE_META;
97
+ const registerAll = bundleModule?.registerAll;
98
+
99
+ if (!metadata || typeof metadata !== 'object' || typeof registerAll !== 'function') {
100
+ throw new Error(
101
+ `Bundle "${bundleName}" missing Bundling V2 exports contract: requires SLICE_BUNDLE_META and registerAll`
102
+ );
103
+ }
104
+
105
+ return { metadata, registerAll };
106
+ }
107
+
108
+ /**
109
+ * 📦 Loads a bundle by name or category
110
+ */
111
+ async loadBundle(bundleName) {
112
+ const resolvedBundleName = this.resolveBundleName(bundleName);
113
+ if (this.loadedBundles.has(resolvedBundleName)) {
114
+ return; // Already loaded
115
+ }
116
+
117
+ return this.loadBundleWithDependencies(resolvedBundleName, new Set());
118
+ }
119
+
120
+ async loadBundleWithDependencies(bundleName, loadingStack = new Set()) {
121
+ const resolvedBundleName = this.resolveBundleName(bundleName);
122
+
123
+ if (this.loadedBundles.has(resolvedBundleName)) {
124
+ return;
125
+ }
126
+
127
+ if (loadingStack.has(resolvedBundleName)) {
128
+ throw new Error(`Circular bundle dependency detected: ${Array.from(loadingStack).join(' -> ')} -> ${resolvedBundleName}`);
129
+ }
130
+
131
+ if (this.bundleLoadPromises.has(resolvedBundleName)) {
132
+ return this.bundleLoadPromises.get(resolvedBundleName);
133
+ }
134
+
135
+ const loadPromise = (async () => {
136
+ loadingStack.add(resolvedBundleName);
137
+ try {
138
+ const bundleInfo = this.getBundleInfo(resolvedBundleName);
139
+ if (!bundleInfo) {
140
+ slice.logger.logWarning('Controller', `Bundle ${resolvedBundleName} not found in configuration`);
141
+ return;
142
+ }
143
+
144
+ const dependencies = this.getBundleDependencies(bundleInfo);
145
+ for (const dependencyName of dependencies) {
146
+ await this.loadBundleWithDependencies(dependencyName, loadingStack);
147
+ }
148
+
149
+ const bundlePath = this.buildBundleImportPath(bundleInfo);
150
+ const bundleModule = await this.importBundleOnce(bundlePath);
151
+ const { metadata, registerAll } = await this.validateBundleModule(bundleModule, resolvedBundleName);
152
+
153
+ const registerResult = await registerAll(this, slice.stylesManager);
154
+ this.registerVendorSharedDependencies(bundleModule, metadata, resolvedBundleName, registerResult);
155
+
156
+ this.loadedBundles.add(resolvedBundleName);
157
+ const loadedBundleKey = metadata.bundleKey;
158
+ if (loadedBundleKey && loadedBundleKey !== resolvedBundleName) {
159
+ this.loadedBundles.add(loadedBundleKey);
160
+ }
161
+
162
+ if (metadata.type === 'critical' || resolvedBundleName === 'critical') {
163
+ this.criticalBundleLoaded = true;
164
+ }
165
+ } finally {
166
+ loadingStack.delete(resolvedBundleName);
167
+ }
168
+ })();
169
+
170
+ this.bundleLoadPromises.set(resolvedBundleName, loadPromise);
171
+ try {
172
+ return await loadPromise;
173
+ } finally {
174
+ this.bundleLoadPromises.delete(resolvedBundleName);
175
+ }
176
+ }
177
+
178
+ resolveBundleName(bundleName) {
179
+ if (typeof bundleName !== 'string' || bundleName.length === 0) {
180
+ return bundleName;
181
+ }
182
+
183
+ if (bundleName.toLowerCase() === 'critical') {
184
+ return 'critical';
185
+ }
186
+
187
+ if (this.isVendorSharedAlias(bundleName) && this.getVendorSharedBundleInfo()) {
188
+ return 'vendor-shared';
189
+ }
190
+
191
+ const routeBundleName = this.findBundleNameByAlias(this.bundleConfig?.bundles?.routes, bundleName);
192
+ if (routeBundleName) {
193
+ return routeBundleName;
194
+ }
195
+
196
+ const sharedBundleName = this.findBundleNameByAlias(this.bundleConfig?.bundles?.shared, bundleName);
197
+ if (sharedBundleName) {
198
+ return sharedBundleName;
199
+ }
200
+
201
+ return bundleName;
202
+ }
203
+
204
+ findBundleNameByAlias(bundleRegistry, bundleName) {
205
+ if (!bundleRegistry || typeof bundleRegistry !== 'object') {
206
+ return null;
207
+ }
208
+
209
+ if (bundleRegistry[bundleName]) {
210
+ return bundleName;
211
+ }
212
+
213
+ const normalizedName = bundleName?.toLowerCase();
214
+ if (!normalizedName) {
215
+ return null;
216
+ }
217
+
218
+ return Object.keys(bundleRegistry).find((key) => key.toLowerCase() === normalizedName) || null;
219
+ }
220
+
221
+ getBundleDependencies(bundleInfo) {
222
+ if (!bundleInfo || !Array.isArray(bundleInfo.dependencies)) {
223
+ return [];
224
+ }
225
+
226
+ return bundleInfo.dependencies.filter((dependency) => typeof dependency === 'string' && dependency.length > 0);
227
+ }
228
+
229
+ findBundleEntryByName(bundleRegistry, bundleName) {
230
+ if (!bundleRegistry || typeof bundleRegistry !== 'object') {
231
+ return null;
232
+ }
233
+
234
+ if (bundleRegistry[bundleName]) {
235
+ return bundleRegistry[bundleName];
236
+ }
237
+
238
+ const normalizedName = bundleName?.toLowerCase();
239
+ if (!normalizedName) {
240
+ return null;
241
+ }
242
+
243
+ const matchedKey = Object.keys(bundleRegistry).find((key) => key.toLowerCase() === normalizedName);
244
+ return matchedKey ? bundleRegistry[matchedKey] : null;
245
+ }
246
+
247
+ getBundleInfo(bundleName) {
248
+ if (bundleName === 'critical') {
249
+ return this.bundleConfig?.bundles?.critical || null;
250
+ }
251
+
252
+ if (this.isVendorSharedAlias(bundleName)) {
253
+ return this.getVendorSharedBundleInfo();
254
+ }
255
+
256
+ return (
257
+ this.findBundleEntryByName(this.bundleConfig?.bundles?.routes, bundleName)
258
+ || this.findBundleEntryByName(this.bundleConfig?.bundles?.shared, bundleName)
259
+ );
260
+ }
261
+
262
+ getVendorSharedBundleInfo() {
263
+ if (this.bundleConfig?.bundles?.vendorShared && typeof this.bundleConfig.bundles.vendorShared === 'object') {
264
+ return this.bundleConfig.bundles.vendorShared;
265
+ }
266
+
267
+ return this.findBundleEntryByName(this.bundleConfig?.bundles?.shared, 'vendor-shared');
268
+ }
269
+
270
+ isVendorSharedAlias(bundleName) {
271
+ if (typeof bundleName !== 'string') {
272
+ return false;
273
+ }
274
+
275
+ const normalized = bundleName.toLowerCase();
276
+ return normalized === 'vendor-shared' || normalized === 'vendorshared';
277
+ }
278
+
279
+ registerVendorSharedDependencies(bundleModule, metadata, bundleName, registerResult) {
280
+ const isVendorShared = this.isVendorSharedBundleName(metadata?.bundleKey)
281
+ || this.isVendorSharedBundleName(bundleName)
282
+ || metadata?.registerVendorSharedDependencies === true;
283
+
284
+ if (!isVendorShared) {
285
+ return;
286
+ }
287
+
288
+ let sharedDeps = bundleModule?.SLICE_SHARED_DEPS;
289
+ if (!sharedDeps && registerResult && typeof registerResult === 'object') {
290
+ sharedDeps = registerResult.SLICE_SHARED_DEPS
291
+ || registerResult.SLICE_BUNDLE_DEPENDENCIES
292
+ || registerResult;
293
+ }
294
+
295
+ if (!sharedDeps || typeof sharedDeps !== 'object') {
296
+ return;
297
+ }
298
+
299
+ if (!window.__SLICE_SHARED_DEPS__ || typeof window.__SLICE_SHARED_DEPS__ !== 'object') {
300
+ window.__SLICE_SHARED_DEPS__ = {};
301
+ }
302
+
303
+ Object.assign(window.__SLICE_SHARED_DEPS__, sharedDeps);
304
+ }
305
+
306
+ isVendorSharedBundleName(bundleName) {
307
+ return typeof bundleName === 'string' && bundleName.toLowerCase() === 'vendor-shared';
308
+ }
309
+
310
+ /**
311
+ * 📦 Registers a bundle's components (called automatically by bundle files)
312
+ */
313
+ registerBundleLegacy(bundle) {
314
+ const { components, metadata } = bundle;
315
+
316
+ slice.logger.logInfo('Controller', `📦 Registering bundle: ${metadata.type} (${metadata.componentCount} components)`);
317
+
318
+ // Phase 1: Register templates and CSS for all components first
319
+ for (const [componentName, componentData] of Object.entries(components)) {
320
+ try {
321
+ // Register HTML template
322
+ if (componentData.html !== undefined && !this.templates.has(componentName)) {
323
+ const template = document.createElement('template');
324
+ template.innerHTML = componentData.html || '';
325
+ this.templates.set(componentName, template);
326
+ }
327
+
328
+ // Register CSS styles
329
+ if (componentData.css !== undefined && !this.requestedStyles.has(componentName)) {
330
+ // Use the existing stylesManager to register component styles
331
+ if (window.slice && window.slice.stylesManager) {
332
+ window.slice.stylesManager.registerComponentStyles(componentName, componentData.css || '');
333
+ this.requestedStyles.add(componentName);
334
+ }
335
+ }
336
+ } catch (error) {
337
+ slice.logger.logError('Controller', `❌ Failed to register assets for ${componentName}`, error);
338
+ }
339
+ }
340
+
341
+ // Phase 2: Evaluate all external file dependencies
342
+ const processedDeps = new Set();
343
+ for (const [componentName, componentData] of Object.entries(components)) {
344
+ if (componentData.dependencies) {
345
+ for (const [depName, depContent] of Object.entries(componentData.dependencies)) {
346
+ if (!processedDeps.has(depName)) {
347
+ try {
348
+ // Convert ES6 exports to global assignments
349
+ let processedContent = depContent
350
+ .replace(/export\s+const\s+(\w+)\s*=/g, 'window.$1 =')
351
+ .replace(/export\s+let\s+(\w+)\s*=/g, 'window.$1 =')
352
+ .replace(/export\s+var\s+(\w+)\s*=/g, 'window.$1 =')
353
+ .replace(/export\s+function\s+(\w+)/g, 'window.$1 = function')
354
+ .replace(/export\s+default\s+/g, 'window.defaultExport =')
355
+ .replace(/export\s*{\s*([^}]+)\s*}/g, (match, exports) => {
356
+ return exports
357
+ .split(',')
358
+ .map((exp) => {
359
+ const cleanExp = exp.trim();
360
+ const varName = cleanExp.split(' as ')[0].trim();
361
+ return `window.${varName} = ${varName};`;
362
+ })
363
+ .join('\n');
364
+ })
365
+ // Remove any remaining export keywords
366
+ .replace(/^\s*export\s+/gm, '');
367
+
368
+ // Evaluate the dependency
369
+ try {
370
+ new Function('slice', 'customElements', 'window', 'document', processedContent)(
371
+ window.slice,
372
+ window.customElements,
373
+ window,
374
+ window.document
375
+ );
376
+ } catch (evalError) {
377
+ slice.logger.error('Controller', `Failed to evaluate processed dependency ${depName}`, evalError);
378
+ slice.logger.warn('Controller', `Processed content preview: ${processedContent.substring(0, 200)}`);
379
+ try {
380
+ new Function('slice', 'customElements', 'window', 'document', depContent)(
381
+ window.slice,
382
+ window.customElements,
383
+ window,
384
+ window.document
385
+ );
386
+ slice.logger.info('Controller', `Fallback evaluation succeeded for ${depName}`);
387
+ } catch (fallbackError) {
388
+ slice.logger.error('Controller', `Fallback evaluation also failed for ${depName}`, fallbackError);
389
+ }
390
+ }
391
+
392
+ processedDeps.add(depName);
393
+ slice.logger.info('Controller', `Dependency loaded: ${depName}`);
394
+ } catch (depError) {
395
+ slice.logger.error('Controller', `Failed to load dependency ${depName} for ${componentName}`, depError);
396
+ }
397
+ }
398
+ }
399
+ }
400
+ }
401
+
402
+ // Phase 3: Evaluate all component classes (now that dependencies are available)
403
+ for (const [componentName, componentData] of Object.entries(components)) {
404
+ // For JavaScript classes, we need to evaluate the code
405
+ if (componentData.js && !this.classes.has(componentName)) {
406
+ try {
407
+ // Create evaluation context with dependencies
408
+ let evalCode = componentData.js;
409
+
410
+ // Prepend dependencies to make them available
411
+ if (componentData.dependencies) {
412
+ const depCode = Object.entries(componentData.dependencies)
413
+ .map(([depName, depContent]) => {
414
+ // Convert ES6 exports to global assignments
415
+ return depContent
416
+ .replace(/export\s+const\s+(\w+)\s*=/g, 'window.$1 =')
417
+ .replace(/export\s+let\s+(\w+)\s*=/g, 'window.$1 =')
418
+ .replace(/export\s+function\s+(\w+)/g, 'window.$1 = function')
419
+ .replace(/export\s+default\s+/g, 'window.defaultExport =')
420
+ .replace(/export\s*{\s*([^}]+)\s*}/g, (match, exports) => {
421
+ return exports
422
+ .split(',')
423
+ .map((exp) => {
424
+ const cleanExp = exp.trim();
425
+ return `window.${cleanExp} = ${cleanExp};`;
426
+ })
427
+ .join('\n');
428
+ });
429
+ })
430
+ .join('\n\n');
431
+
432
+ evalCode = depCode + '\n\n' + evalCode;
433
+ }
434
+
435
+ // Evaluate the complete code
436
+ const componentClass = new Function(
437
+ 'slice',
438
+ 'customElements',
439
+ 'window',
440
+ 'document',
441
+ `
442
+ "use strict";
443
+ ${evalCode}
444
+ return ${componentName};
445
+ `
446
+ )(window.slice, window.customElements, window, window.document);
447
+
448
+ if (componentClass) {
449
+ this.classes.set(componentName, componentClass);
450
+ slice.logger.logInfo('Controller', `📝 Class registered for: ${componentName}`);
451
+ }
452
+ } catch (error) {
453
+ slice.logger.error('Controller', `Failed to evaluate class for ${componentName}`, error);
454
+ slice.logger.warn('Controller', `Code that failed: ${componentData.js.substring(0, 200) + '...'}`);
455
+ }
456
+ }
457
+ }
458
+ }
459
+
460
+ /**
461
+ * 📦 New bundle registration method (simplified and robust)
462
+ */
463
+ registerBundle(bundle) {
464
+ const validation = this.validateBundle(bundle);
465
+ if (!validation.isValid) {
466
+ slice.logger.error('Controller', `Bundle validation failed: ${validation.error}`);
467
+ return Promise.resolve(false);
468
+ }
469
+
470
+ // Set tracking flags synchronously before any async work, so callers that
471
+ // await import() see the flags set immediately when the Promise resolves.
472
+ const { components, metadata } = bundle;
473
+ const bundleKey = metadata?.bundleKey;
474
+ if (bundleKey) {
475
+ this.loadedBundles.add(bundleKey);
476
+ if (metadata?.type === 'critical') {
477
+ this.criticalBundleLoaded = true;
478
+ }
479
+ }
480
+
481
+ slice.logger.logInfo('Controller', `📦 Registering bundle: ${metadata.type} (${metadata.componentCount} components)`);
482
+
483
+ const entries = Object.entries(components);
484
+ const chunkSize = 50;
485
+ let index = 0;
486
+
487
+ return new Promise((resolve) => {
488
+ const processChunk = () => {
489
+ try {
490
+ const sliceEntries = entries.slice(index, index + chunkSize);
491
+
492
+ for (const [componentName, componentData] of sliceEntries) {
493
+ try {
494
+ if (componentData.html !== undefined && !this.templates.has(componentName)) {
495
+ const template = document.createElement('template');
496
+ template.innerHTML = componentData.html || '';
497
+ this.templates.set(componentName, template);
498
+ }
499
+
500
+ if (componentData.css !== undefined && !this.requestedStyles.has(componentName)) {
501
+ if (window.slice && window.slice.stylesManager) {
502
+ window.slice.stylesManager.registerComponentStyles(componentName, componentData.css || '');
503
+ this.requestedStyles.add(componentName);
504
+ }
505
+ }
506
+
507
+ if (componentData.class && !this.classes.has(componentName)) {
508
+ const registeredName = componentData.isFramework
509
+ ? `Framework/Structural/${componentName}`
510
+ : componentName;
511
+ this.classes.set(registeredName, componentData.class);
512
+ if (componentName === 'Loading') {
513
+ slice.logger.logInfo('Controller', `Bundle class registered: Loading (registeredName=${registeredName})`);
514
+ }
515
+ if (componentName === 'InputSearchDocs' || componentName === 'MainMenu') {
516
+ slice.logger.logInfo('Controller', `Bundle class registered: ${componentName}`);
517
+ }
518
+ }
519
+ } catch (error) {
520
+ slice.logger.error('Controller', `Failed to register component ${componentName}`, error);
521
+ }
522
+ }
523
+
524
+ index += chunkSize;
525
+ if (index < entries.length) {
526
+ if (typeof requestIdleCallback === 'function') {
527
+ requestIdleCallback(processChunk);
528
+ } else {
529
+ setTimeout(processChunk, 0);
530
+ }
531
+ return;
532
+ }
533
+
534
+ slice.logger.info('Controller', `Bundle registration completed: ${metadata.componentCount} components processed`);
535
+ resolve(true);
536
+ } catch (error) {
537
+ slice.logger.error('Controller', 'Fatal error in registerBundle chunk processing', error);
538
+ resolve(false);
539
+ }
540
+ };
541
+
542
+ processChunk();
543
+ });
544
+ }
545
+
546
+ /**
547
+ * Validates bundle structure before registering.
548
+ * @param {object} bundle
549
+ * @returns {{isValid: boolean, error?: string}}
550
+ */
551
+ validateBundle(bundle) {
552
+ if (!bundle || typeof bundle !== 'object') {
553
+ return { isValid: false, error: 'Bundle payload is invalid' };
554
+ }
555
+
556
+ if (!bundle.metadata || typeof bundle.metadata !== 'object') {
557
+ return { isValid: false, error: 'Bundle metadata missing' };
558
+ }
559
+
560
+ if (!bundle.components || typeof bundle.components !== 'object') {
561
+ return { isValid: false, error: 'Bundle components missing' };
562
+ }
563
+
564
+ if (typeof bundle.metadata.componentCount !== 'number') {
565
+ return { isValid: false, error: 'Bundle metadata missing componentCount' };
566
+ }
567
+
568
+ if (bundle.metadata.componentCount !== Object.keys(bundle.components).length) {
569
+ return { isValid: false, error: 'Bundle component count mismatch' };
570
+ }
571
+
572
+ const maxComponents = 5000;
573
+ if (bundle.metadata.componentCount > maxComponents) {
574
+ return { isValid: false, error: 'Bundle component count exceeds limit' };
575
+ }
576
+
577
+ return { isValid: true };
578
+ }
579
+
580
+ /**
581
+ * 📦 Determines which bundle to load for a component
582
+ */
583
+ getBundleForComponent(componentName) {
584
+ if (!this.bundleConfig?.bundles) return null;
585
+
586
+ // Check if component is in critical bundle
587
+ if (this.bundleConfig.bundles.critical?.components?.includes(componentName)) {
588
+ return 'critical';
589
+ }
590
+
591
+ // Find component in route bundles
592
+ if (this.bundleConfig.bundles.routes) {
593
+ for (const [bundleName, bundleInfo] of Object.entries(this.bundleConfig.bundles.routes)) {
594
+ if (bundleInfo.components?.includes(componentName)) {
595
+ return bundleName;
596
+ }
597
+ }
598
+ }
599
+
600
+ return null;
601
+ }
602
+
603
+ /**
604
+ * 📦 Checks if a component is available from loaded bundles
605
+ */
606
+ isComponentFromBundle(componentName) {
607
+ if (!this.bundleConfig?.bundles) return false;
608
+
609
+ // Check critical bundle
610
+ if (this.bundleConfig.bundles.critical?.components?.includes(componentName)) {
611
+ return this.criticalBundleLoaded;
612
+ }
613
+
614
+ // Check route bundles
615
+ if (this.bundleConfig.bundles.routes) {
616
+ for (const [bundleName, bundleInfo] of Object.entries(this.bundleConfig.bundles.routes)) {
617
+ if (bundleInfo.components?.includes(componentName)) {
618
+ return this.loadedBundles.has(bundleName);
619
+ }
620
+ }
621
+ }
622
+
623
+ return false;
624
+ }
625
+
626
+ /**
627
+ * 📦 Gets component data from loaded bundles
628
+ */
629
+ getComponentFromBundle(componentName) {
630
+ if (!this.bundleConfig?.bundles) return null;
631
+
632
+ // Find component in any loaded bundle
633
+ const allBundles = [
634
+ { name: 'critical', data: this.bundleConfig.bundles.critical },
635
+ ...Object.entries(this.bundleConfig.bundles.routes || {}).map(([name, data]) => ({ name, data })),
636
+ ];
637
+
638
+ for (const { name: bundleName, data: bundleData } of allBundles) {
639
+ if (bundleData?.components?.includes(componentName) && this.loadedBundles.has(bundleName)) {
640
+ // Find the bundle file and extract component data
641
+ // This is a simplified version - in practice you'd need to access the loaded bundle data
642
+ return { bundleName, componentName };
643
+ }
644
+ }
645
+
646
+ return null;
647
+ }
648
+
649
+ logActiveComponents() {
650
+ this.activeComponents.forEach((component) => {
651
+ let parent = component.parentComponent;
652
+ let parentName = parent ? parent.constructor.name : null;
653
+ });
654
+ }
655
+
656
+ getTopParentsLinkedToActiveComponents() {
657
+ let topParentsLinkedToActiveComponents = new Map();
658
+ this.activeComponents.forEach((component) => {
659
+ let parent = component.parentComponent;
660
+ while (parent && parent.parentComponent) {
661
+ parent = parent.parentComponent;
662
+ }
663
+ if (!topParentsLinkedToActiveComponents.has(parent)) {
664
+ topParentsLinkedToActiveComponents.set(parent, []);
665
+ }
666
+ topParentsLinkedToActiveComponents.get(parent).push(component);
667
+ });
668
+ return topParentsLinkedToActiveComponents;
669
+ }
670
+
671
+ verifyComponentIds(component) {
672
+ const htmlId = component.id;
673
+
674
+ if (htmlId && htmlId.trim() !== '') {
675
+ if (this.activeComponents.has(htmlId)) {
676
+ slice.logger.logError(
677
+ 'Controller',
678
+ `A component with the same html id attribute is already registered: ${htmlId}`
679
+ );
680
+ return false;
681
+ }
682
+ }
683
+
684
+ let sliceId = component.sliceId;
685
+
686
+ if (sliceId && sliceId.trim() !== '') {
687
+ if (this.activeComponents.has(sliceId)) {
688
+ slice.logger.logError(
689
+ 'Controller',
690
+ `A component with the same slice id attribute is already registered: ${sliceId}`
691
+ );
692
+ return false;
693
+ }
694
+ } else {
695
+ sliceId = `${component.constructor.name[0].toLowerCase()}${component.constructor.name.slice(1)}-${this.idCounter}`;
696
+ component.sliceId = sliceId;
697
+ this.idCounter++;
698
+ }
699
+
700
+ component.sliceId = sliceId;
701
+ return true;
702
+ }
703
+
704
+ /**
705
+ * Registra un componente y actualiza el índice de relaciones padre-hijo
706
+ * 🚀 OPTIMIZADO: Ahora mantiene childrenIndex y precalcula profundidad
707
+ */
708
+ registerComponent(component, parent = null) {
709
+ component.parentComponent = parent;
710
+
711
+ // 🚀 OPTIMIZACIÓN: Precalcular y guardar profundidad
712
+ component._depth = parent ? (parent._depth || 0) + 1 : 0;
713
+
714
+ // Registrar en activeComponents
715
+ this.activeComponents.set(component.sliceId, component);
716
+
717
+ // Exponer sliceId como atributo HTML para búsqueda por DOM (destroyByContainer, etc.)
718
+ if (typeof component.setAttribute === 'function') {
719
+ component.setAttribute('slice-id', component.sliceId);
720
+ }
721
+
722
+ // 🚀 OPTIMIZACIÓN: Actualizar índice inverso de hijos
723
+ if (parent) {
724
+ if (!this.childrenIndex.has(parent.sliceId)) {
725
+ this.childrenIndex.set(parent.sliceId, new Set());
726
+ }
727
+ this.childrenIndex.get(parent.sliceId).add(component.sliceId);
728
+ }
729
+
730
+ return true;
731
+ }
732
+
733
+ registerComponentsRecursively(component, parent = null) {
734
+ // Assign parent if not already set
735
+ if (!component.parentComponent) {
736
+ component.parentComponent = parent;
737
+ }
738
+
739
+ // Recursively assign parent to children
740
+ component.querySelectorAll('*').forEach((child) => {
741
+ if (child.tagName.startsWith('SLICE-')) {
742
+ // Only the call that establishes the DIRECT parent link feeds the
743
+ // index — the depth-first recursion sets a node's parent before an
744
+ // outer ancestor's forEach reaches it, so `component` here is the
745
+ // immediate enclosing component.
746
+ if (!child.parentComponent) {
747
+ child.parentComponent = component;
748
+ // 🔁 Maintain childrenIndex so destroyComponent(parent) cascades
749
+ // to children built via slice.build (which registers them WITHOUT
750
+ // a parent, leaving the index otherwise empty). Without this, a
751
+ // parent's destroy never finds and never cleans up — its children.
752
+ if (child.sliceId && component.sliceId) {
753
+ if (!this.childrenIndex.has(component.sliceId)) {
754
+ this.childrenIndex.set(component.sliceId, new Set());
755
+ }
756
+ this.childrenIndex.get(component.sliceId).add(child.sliceId);
757
+ child._depth = (component._depth || 0) + 1;
758
+ }
759
+ }
760
+ this.registerComponentsRecursively(child, component);
761
+ }
762
+ });
763
+ }
764
+
765
+ /**
766
+ * Get a registered component by sliceId.
767
+ * @param {string} sliceId
768
+ * @returns {HTMLElement|undefined}
769
+ */
770
+ getComponent(sliceId) {
771
+ return this.activeComponents.get(sliceId);
772
+ }
773
+
774
+ /**
775
+ * Get-or-create a single instance keyed by sliceId, deduplicating concurrent
776
+ * builds. Returns the existing instance if already registered, the in-flight
777
+ * promise if a build is underway, or otherwise memoizes a fresh build via
778
+ * `builder` (an injected closure the controller never builds by itself).
779
+ *
780
+ * The in-flight promise is removed once it settles, so a failed build (which
781
+ * resolves to `null` and never registers in activeComponents) can be retried
782
+ * by a later call and never poisons the registry.
783
+ *
784
+ * @param {string} sliceId
785
+ * @param {() => Promise<any>} builder
786
+ * @returns {any|Promise<any>} instance (sync) or Promise<instance>
787
+ */
788
+ getOrCreate(sliceId, builder) {
789
+ const existing = this.activeComponents.get(sliceId);
790
+ if (existing) return existing;
791
+
792
+ const pending = this._pendingBuilds.get(sliceId);
793
+ if (pending) return pending;
794
+
795
+ const promise = Promise.resolve(builder())
796
+ .finally(() => this._pendingBuilds.delete(sliceId));
797
+ this._pendingBuilds.set(sliceId, promise);
798
+ return promise;
799
+ }
800
+
801
+ loadTemplateToComponent(component) {
802
+ const className = component.constructor.name;
803
+ const template = this.templates.get(className);
804
+
805
+ if (!template) {
806
+ slice.logger.error('Controller', `Template not found for component: ${className}`);
807
+ return;
808
+ }
809
+
810
+ component.innerHTML = template.innerHTML;
811
+ return component;
812
+ }
813
+
814
+ getComponentCategory(componentSliceId) {
815
+ return this.componentCategories.get(componentSliceId);
816
+ }
817
+
818
+ /**
819
+ * Fetch component resources (html, css, styles, theme).
820
+ * @param {string} componentName
821
+ * @param {'html'|'css'|'theme'|'styles'} resourceType
822
+ * @param {string} [componentCategory]
823
+ * @param {string} [customPath]
824
+ * @returns {Promise<string>}
825
+ */
826
+ async fetchText(componentName, resourceType, componentCategory, customPath) {
827
+ try {
828
+ const baseUrl = window.location.origin;
829
+ let path;
830
+
831
+ if (!componentCategory) {
832
+ componentCategory = this.componentCategories.get(componentName);
833
+ }
834
+
835
+ let isVisual = resourceType === 'html' || resourceType === 'css';
836
+
837
+ if (isVisual) {
838
+ if (slice.paths.components[componentCategory]) {
839
+ path = `${baseUrl}${slice.paths.components[componentCategory].path}/${componentName}`;
840
+ resourceType === 'html' ? (path += `/${componentName}.html`) : (path += `/${componentName}.css`);
841
+ } else {
842
+ if (componentCategory === 'Structural') {
843
+ path = `${baseUrl}/Slice/Components/Structural/${componentName}`;
844
+ resourceType === 'html' ? (path += `/${componentName}.html`) : (path += `/${componentName}.css`);
845
+ } else {
846
+ throw new Error(`Component category '${componentCategory}' not found in paths configuration`);
847
+ }
848
+ }
849
+ }
850
+
851
+ if (resourceType === 'theme') {
852
+ path = `${baseUrl}${slice.paths.themes}/${componentName}.css`;
853
+ }
854
+
855
+ if (resourceType === 'styles') {
856
+ path = `${baseUrl}${slice.paths.styles}/${componentName}.css`;
857
+ }
858
+
859
+ if (customPath) {
860
+ path = customPath;
861
+ }
862
+
863
+ slice.logger.logInfo('Controller', `Fetching ${resourceType} from: ${path}`);
864
+
865
+ const response = await fetch(path);
866
+
867
+ if (!response.ok) {
868
+ throw new Error(`Failed to fetch ${path}: ${response.statusText}`);
869
+ }
870
+
871
+ const content = await response.text();
872
+ slice.logger.logInfo('Controller', `Successfully fetched ${resourceType} for ${componentName}`);
873
+ return content;
874
+ } catch (error) {
875
+ slice.logger.logError('Controller', `Error fetching ${resourceType} for component ${componentName}:`, error);
876
+ throw error;
877
+ }
878
+ }
879
+
880
+ /**
881
+ * Apply props to a component using static defaults and setters.
882
+ * @param {HTMLElement} component
883
+ * @param {Object} props
884
+ * @returns {void}
885
+ */
886
+ setComponentProps(component, props) {
887
+ const ComponentClass = component.constructor;
888
+ const componentName = ComponentClass.name;
889
+
890
+ // Aplicar defaults si tiene static props
891
+ if (ComponentClass.props) {
892
+ this.applyDefaultProps(component, ComponentClass.props, props);
893
+ }
894
+
895
+ // Validar solo en desarrollo
896
+ if (ComponentClass.props && !slice.isProduction()) {
897
+ this.validatePropsInDevelopment(ComponentClass, props, componentName);
898
+ }
899
+
900
+ // Aplicar props
901
+ for (const prop in props) {
902
+ component[`_${prop}`] = null;
903
+ component[prop] = props[prop];
904
+ }
905
+ }
906
+
907
+ getComponentPropsForDebugger(component) {
908
+ const ComponentClass = component.constructor;
909
+
910
+ if (ComponentClass.props) {
911
+ return {
912
+ availableProps: Object.keys(ComponentClass.props),
913
+ propsConfig: ComponentClass.props,
914
+ usedProps: this.extractUsedProps(component, ComponentClass.props),
915
+ };
916
+ } else {
917
+ return {
918
+ availableProps: this.extractUsedProps(component),
919
+ propsConfig: null,
920
+ usedProps: this.extractUsedProps(component),
921
+ };
922
+ }
923
+ }
924
+
925
+ applyDefaultProps(component, staticProps, providedProps) {
926
+ Object.entries(staticProps).forEach(([prop, config]) => {
927
+ if (config.default !== undefined && !(prop in (providedProps || {}))) {
928
+ component[`_${prop}`] = null;
929
+ component[prop] = config.default;
930
+ }
931
+ });
932
+ }
933
+
934
+ validatePropsInDevelopment(ComponentClass, providedProps, componentName) {
935
+ const staticProps = ComponentClass.props;
936
+ const usedProps = Object.keys(providedProps || {});
937
+
938
+ const availableProps = Object.keys(staticProps);
939
+ const unknownProps = usedProps.filter((prop) => !availableProps.includes(prop));
940
+
941
+ if (unknownProps.length > 0) {
942
+ slice.logger.logWarning(
943
+ 'PropsValidator',
944
+ `${componentName}: Unknown props [${unknownProps.join(', ')}]. Available: [${availableProps.join(', ')}]`
945
+ );
946
+ }
947
+
948
+ const requiredProps = Object.entries(staticProps)
949
+ .filter(([_, config]) => config.required)
950
+ .map(([prop, _]) => prop);
951
+
952
+ const missingRequired = requiredProps.filter((prop) => !(prop in (providedProps || {})));
953
+ if (missingRequired.length > 0) {
954
+ slice.logger.logError(componentName, `Missing required props: [${missingRequired.join(', ')}]`);
955
+ }
956
+
957
+ const invalidAllowedValueProps = collectInvalidAllowedValueProps(staticProps, providedProps);
958
+ invalidAllowedValueProps.forEach(({ propName, value, allowedValues }) => {
959
+ const safeValue = (() => { try { return JSON.stringify(value); } catch { return String(value); } })();
960
+ slice.logger.error(
961
+ componentName,
962
+ `Invalid value for prop "${propName}": ${safeValue}. Allowed values: ${formatAllowedValuesForLog(allowedValues)}`
963
+ );
964
+ });
965
+ }
966
+
967
+ extractUsedProps(component, staticProps = null) {
968
+ const usedProps = {};
969
+
970
+ if (staticProps) {
971
+ Object.keys(staticProps).forEach((prop) => {
972
+ if (component[prop] !== undefined) {
973
+ usedProps[prop] = component[prop];
974
+ }
975
+ });
976
+ } else {
977
+ Object.getOwnPropertyNames(component).forEach((key) => {
978
+ if (key.startsWith('_') && key !== '_isActive') {
979
+ const propName = key.substring(1);
980
+ usedProps[propName] = component[propName];
981
+ }
982
+ });
983
+ }
984
+
985
+ return usedProps;
986
+ }
987
+
988
+ // ============================================================================
989
+ // 🚀 MÉTODOS DE DESTRUCCIÓN OPTIMIZADOS
990
+ // ============================================================================
991
+
992
+ /**
993
+ * Encuentra recursivamente todos los hijos de un componente
994
+ * 🚀 OPTIMIZADO: O(m) en lugar de O(n*d) - usa childrenIndex
995
+ * @param {string} parentSliceId - sliceId del componente padre
996
+ * @param {Set<string>} collected - Set de sliceIds ya recolectados
997
+ * @returns {Set<string>} Set de todos los sliceIds de componentes hijos
998
+ */
999
+ findAllChildComponents(parentSliceId, collected = new Set()) {
1000
+ // 🚀 Buscar directamente en el índice: O(1)
1001
+ const children = this.childrenIndex.get(parentSliceId);
1002
+
1003
+ if (!children) return collected;
1004
+
1005
+ // 🚀 Iterar solo los hijos directos: O(k) donde k = número de hijos
1006
+ for (const childSliceId of children) {
1007
+ collected.add(childSliceId);
1008
+ // Recursión solo sobre hijos, no todos los componentes
1009
+ this.findAllChildComponents(childSliceId, collected);
1010
+ }
1011
+
1012
+ return collected;
1013
+ }
1014
+
1015
+ /**
1016
+ * Encuentra recursivamente todos los componentes dentro de un contenedor DOM
1017
+ * Útil para destroyByContainer cuando no tenemos el sliceId del padre
1018
+ * @param {HTMLElement} container - Elemento contenedor
1019
+ * @param {Set<string>} collected - Set de sliceIds ya recolectados
1020
+ * @returns {Set<string>} Set de todos los sliceIds encontrados
1021
+ */
1022
+ findAllNestedComponentsInContainer(container, collected = new Set()) {
1023
+ // Buscar todos los elementos con slice-id en el contenedor
1024
+ const sliceComponents = container.querySelectorAll('[slice-id]');
1025
+
1026
+ sliceComponents.forEach((element) => {
1027
+ const sliceId = element.getAttribute('slice-id') || element.sliceId;
1028
+ if (sliceId && this.activeComponents.has(sliceId)) {
1029
+ collected.add(sliceId);
1030
+ // 🚀 Usar índice para buscar hijos recursivamente
1031
+ this.findAllChildComponents(sliceId, collected);
1032
+ }
1033
+ });
1034
+
1035
+ return collected;
1036
+ }
1037
+
1038
+ /**
1039
+ * Destruye uno o múltiples componentes DE FORMA RECURSIVA
1040
+ * 🚀 OPTIMIZADO: O(m log m) en lugar de O(n*d + m log m)
1041
+ * @param {HTMLElement|Array<HTMLElement>|string|Array<string>} components
1042
+ * @returns {number} Cantidad de componentes destruidos (incluyendo hijos)
1043
+ */
1044
+ destroyComponent(components) {
1045
+ const toDestroy = Array.isArray(components) ? components : [components];
1046
+ const allSliceIdsToDestroy = new Set();
1047
+
1048
+ // PASO 1: Recolectar todos los componentes padres y sus hijos recursivamente
1049
+ for (const item of toDestroy) {
1050
+ let sliceId = null;
1051
+
1052
+ if (typeof item === 'string') {
1053
+ if (!this.activeComponents.has(item)) {
1054
+ slice.logger.logWarning('Controller', `Component with sliceId "${item}" not found`);
1055
+ continue;
1056
+ }
1057
+ sliceId = item;
1058
+ } else if (item && item.sliceId) {
1059
+ sliceId = item.sliceId;
1060
+ } else {
1061
+ slice.logger.logWarning('Controller', `Invalid component or sliceId provided to destroyComponent`);
1062
+ continue;
1063
+ }
1064
+
1065
+ allSliceIdsToDestroy.add(sliceId);
1066
+
1067
+ // 🚀 OPTIMIZADO: Usa childrenIndex en lugar de recorrer todos los componentes
1068
+ this.findAllChildComponents(sliceId, allSliceIdsToDestroy);
1069
+ }
1070
+
1071
+ // PASO 2: Ordenar por profundidad (más profundos primero)
1072
+ // 🚀 OPTIMIZADO: Usa _depth precalculada en lugar de calcularla cada vez
1073
+ const sortedSliceIds = Array.from(allSliceIdsToDestroy).sort((a, b) => {
1074
+ const compA = this.activeComponents.get(a);
1075
+ const compB = this.activeComponents.get(b);
1076
+
1077
+ if (!compA || !compB) return 0;
1078
+
1079
+ // 🚀 O(1) en lugar de O(d) - usa profundidad precalculada
1080
+ return (compB._depth || 0) - (compA._depth || 0);
1081
+ });
1082
+
1083
+ let destroyedCount = 0;
1084
+
1085
+ // PASO 3: Destruir en orden correcto (hijos antes que padres)
1086
+ for (const sliceId of sortedSliceIds) {
1087
+ const component = this.activeComponents.get(sliceId);
1088
+
1089
+ if (!component) continue;
1090
+
1091
+ // Ejecutar hook beforeDestroy si existe
1092
+ if (typeof component.beforeDestroy === 'function') {
1093
+ try {
1094
+ component.beforeDestroy();
1095
+ } catch (error) {
1096
+ slice.logger.logError('Controller', `Error in beforeDestroy for ${sliceId}`, error);
1097
+ }
1098
+ }
1099
+
1100
+ // Limpiar suscripciones de eventos del componente
1101
+ if (slice.events) {
1102
+ slice.events.cleanupComponent(sliceId);
1103
+ }
1104
+
1105
+ // 🚀 Limpiar del índice de hijos
1106
+ this.childrenIndex.delete(sliceId);
1107
+
1108
+ // Si tiene padre, remover de la lista de hijos del padre
1109
+ if (component.parentComponent) {
1110
+ const parentChildren = this.childrenIndex.get(component.parentComponent.sliceId);
1111
+ if (parentChildren) {
1112
+ parentChildren.delete(sliceId);
1113
+ // Si el padre no tiene más hijos, eliminar entrada vacía
1114
+ if (parentChildren.size === 0) {
1115
+ this.childrenIndex.delete(component.parentComponent.sliceId);
1116
+ }
1117
+ }
1118
+ }
1119
+
1120
+ // Eliminar del mapa de componentes activos
1121
+ this.activeComponents.delete(sliceId);
1122
+
1123
+ // Remover del DOM si está conectado
1124
+ if (component.isConnected) {
1125
+ component.remove();
1126
+ }
1127
+
1128
+ destroyedCount++;
1129
+ }
1130
+
1131
+ if (destroyedCount > 0) {
1132
+ slice.logger.logInfo('Controller', `Destroyed ${destroyedCount} component(s) recursively`);
1133
+ }
1134
+
1135
+ return destroyedCount;
1136
+ }
1137
+
1138
+ /**
1139
+ * Destruye todos los componentes Slice dentro de un contenedor (RECURSIVO)
1140
+ * 🚀 OPTIMIZADO: Usa el índice inverso para búsqueda rápida
1141
+ * @param {HTMLElement} container - Elemento contenedor
1142
+ * @returns {number} Cantidad de componentes destruidos
1143
+ */
1144
+ destroyByContainer(container) {
1145
+ if (!container) {
1146
+ slice.logger.logWarning('Controller', 'No container provided to destroyByContainer');
1147
+ return 0;
1148
+ }
1149
+
1150
+ // 🚀 Recolectar componentes usando índice optimizado
1151
+ const allSliceIds = this.findAllNestedComponentsInContainer(container);
1152
+
1153
+ if (allSliceIds.size === 0) {
1154
+ return 0;
1155
+ }
1156
+
1157
+ // Destruir usando el método principal optimizado
1158
+ const count = this.destroyComponent(Array.from(allSliceIds));
1159
+
1160
+ if (count > 0) {
1161
+ slice.logger.logInfo('Controller', `Destroyed ${count} component(s) from container (including nested)`);
1162
+ }
1163
+
1164
+ return count;
1165
+ }
1166
+
1167
+ /**
1168
+ * Destruye componentes cuyos sliceId coincidan con un patrón (RECURSIVO)
1169
+ * 🚀 OPTIMIZADO: Usa destrucción optimizada
1170
+ * @param {string|RegExp} pattern - Patrón a buscar
1171
+ * @returns {number} Cantidad de componentes destruidos
1172
+ */
1173
+ destroyByPattern(pattern) {
1174
+ const componentsToDestroy = [];
1175
+ const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
1176
+
1177
+ for (const [sliceId, component] of this.activeComponents) {
1178
+ if (regex.test(sliceId)) {
1179
+ componentsToDestroy.push(component);
1180
+ }
1181
+ }
1182
+
1183
+ if (componentsToDestroy.length === 0) {
1184
+ return 0;
1185
+ }
1186
+
1187
+ const count = this.destroyComponent(componentsToDestroy);
1188
+
1189
+ if (count > 0) {
1190
+ slice.logger.logInfo(
1191
+ 'Controller',
1192
+ `Destroyed ${count} component(s) matching pattern: ${pattern} (including nested)`
1193
+ );
1194
+ }
1195
+
1196
+ return count;
1197
+ }
1198
+ }