slicejs-web-framework 2.2.13 → 2.3.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,890 +1,925 @@
1
- import components from '/Components/components.js';
2
-
3
- export default class Controller {
4
- constructor() {
5
- this.componentCategories = new Map(Object.entries(components));
6
- this.templates = new Map();
7
- this.classes = new Map();
8
- this.requestedStyles = new Set(); // ✅ CRÍTICO: Para tracking de CSS cargados
9
- this.activeComponents = new Map();
10
-
11
- // 🚀 OPTIMIZACIÓN: Índice inverso para búsqueda rápida de hijos
12
- // parentSliceId → Set<childSliceId>
13
- this.childrenIndex = new Map();
14
-
15
- // 📦 Bundle system
16
- this.loadedBundles = new Set();
17
- this.bundleConfig = null;
18
- this.criticalBundleLoaded = false;
19
-
20
- this.idCounter = 0;
21
- }
22
-
23
- /**
24
- * 📦 Initializes bundle system (called automatically when config is loaded)
25
- */
26
- initializeBundles(config = null) {
27
- if (config) {
28
- this.bundleConfig = config;
29
-
30
- // Register critical bundle components if available
31
- if (config.bundles?.critical) {
32
- // The critical bundle should already be loaded, register its components
33
- this.loadedBundles.add('critical');
34
- // Note: Critical bundle registration is handled by the auto-import
35
- }
36
- this.criticalBundleLoaded = true;
37
- } else {
38
- // No bundles available, will use individual component loading
39
- this.bundleConfig = null;
40
- this.criticalBundleLoaded = false;
41
- }
42
- }
43
-
44
- /**
45
- * 📦 Loads a bundle by name or category
46
- */
47
- async loadBundle(bundleName) {
48
- if (this.loadedBundles.has(bundleName)) {
49
- return; // Already loaded
50
- }
51
-
52
- try {
53
- let bundleInfo = this.bundleConfig?.bundles?.routes?.[bundleName];
54
-
55
- if (!bundleInfo && this.bundleConfig?.bundles?.routes) {
56
- const normalizedName = bundleName?.toLowerCase();
57
- const matchedKey = Object.keys(this.bundleConfig.bundles.routes)
58
- .find(key => key.toLowerCase() === normalizedName);
59
- if (matchedKey) {
60
- bundleInfo = this.bundleConfig.bundles.routes[matchedKey];
61
- }
62
- }
63
-
64
- if (!bundleInfo) {
65
- console.warn(`Bundle ${bundleName} not found in configuration`);
66
- return;
67
- }
68
-
69
- const bundlePath = `/bundles/${bundleInfo.file}`;
70
-
71
- // Dynamic import of the bundle
72
- const bundleModule = await import(bundlePath);
73
-
74
- // Manually register components from the imported bundle
75
- if (bundleModule.SLICE_BUNDLE) {
76
- this.registerBundle(bundleModule.SLICE_BUNDLE);
77
- }
78
-
79
- this.loadedBundles.add(bundleName);
80
-
81
- } catch (error) {
82
- console.warn(`Failed to load bundle ${bundleName}:`, error);
83
- }
84
- }
85
-
86
- /**
87
- * 📦 Registers a bundle's components (called automatically by bundle files)
88
- */
89
- registerBundleLegacy(bundle) {
90
- const { components, metadata } = bundle;
91
-
92
- console.log(`📦 Registering bundle: ${metadata.type} (${metadata.componentCount} components)`);
93
-
94
- // Phase 1: Register templates and CSS for all components first
95
- for (const [componentName, componentData] of Object.entries(components)) {
96
- try {
97
- // Register HTML template
98
- if (componentData.html !== undefined && !this.templates.has(componentName)) {
99
- const template = document.createElement('template');
100
- template.innerHTML = componentData.html || '';
101
- this.templates.set(componentName, template);
102
- }
103
-
104
- // Register CSS styles
105
- if (componentData.css !== undefined && !this.requestedStyles.has(componentName)) {
106
- // Use the existing stylesManager to register component styles
107
- if (window.slice && window.slice.stylesManager) {
108
- window.slice.stylesManager.registerComponentStyles(componentName, componentData.css || '');
109
- this.requestedStyles.add(componentName);
110
- }
111
- }
112
- } catch (error) {
113
- console.warn(`❌ Failed to register assets for ${componentName}:`, error);
114
- }
115
- }
116
-
117
- // Phase 2: Evaluate all external file dependencies
118
- const processedDeps = new Set();
119
- for (const [componentName, componentData] of Object.entries(components)) {
120
- if (componentData.dependencies) {
121
- for (const [depName, depContent] of Object.entries(componentData.dependencies)) {
122
- if (!processedDeps.has(depName)) {
123
- try {
124
- // Convert ES6 exports to global assignments
125
- let processedContent = depContent
126
- .replace(/export\s+const\s+(\w+)\s*=/g, 'window.$1 =')
127
- .replace(/export\s+let\s+(\w+)\s*=/g, 'window.$1 =')
128
- .replace(/export\s+var\s+(\w+)\s*=/g, 'window.$1 =')
129
- .replace(/export\s+function\s+(\w+)/g, 'window.$1 = function')
130
- .replace(/export\s+default\s+/g, 'window.defaultExport =')
131
- .replace(/export\s*{\s*([^}]+)\s*}/g, (match, exports) => {
132
- return exports.split(',').map(exp => {
133
- const cleanExp = exp.trim();
134
- const varName = cleanExp.split(' as ')[0].trim();
135
- return `window.${varName} = ${varName};`;
136
- }).join('\n');
137
- })
138
- // Remove any remaining export keywords
139
- .replace(/^\s*export\s+/gm, '');
140
-
141
- // Evaluate the dependency
142
- try {
143
- new Function('slice', 'customElements', 'window', 'document', processedContent)
144
- (window.slice, window.customElements, window, window.document);
145
- } catch (evalError) {
146
- console.warn(`❌ Failed to evaluate processed dependency ${depName}:`, evalError);
147
- console.warn('Processed content preview:', processedContent.substring(0, 200));
148
- // Try evaluating the original content as fallback
149
- try {
150
- new Function('slice', 'customElements', 'window', 'document', depContent)
151
- (window.slice, window.customElements, window, window.document);
152
- console.log(`✅ Fallback evaluation succeeded for ${depName}`);
153
- } catch (fallbackError) {
154
- console.warn(`❌ Fallback evaluation also failed for ${depName}:`, fallbackError);
155
- }
156
- }
157
-
158
- processedDeps.add(depName);
159
- console.log(`📄 Dependency loaded: ${depName}`);
160
- } catch (depError) {
161
- console.warn(`⚠️ Failed to load dependency ${depName} for ${componentName}:`, depError);
162
- }
163
- }
164
- }
165
- }
166
- }
167
-
168
- // Phase 3: Evaluate all component classes (now that dependencies are available)
169
- for (const [componentName, componentData] of Object.entries(components)) {
170
- // For JavaScript classes, we need to evaluate the code
171
- if (componentData.js && !this.classes.has(componentName)) {
172
- try {
173
- // Create evaluation context with dependencies
174
- let evalCode = componentData.js;
175
-
176
- // Prepend dependencies to make them available
177
- if (componentData.dependencies) {
178
- const depCode = Object.entries(componentData.dependencies)
179
- .map(([depName, depContent]) => {
180
- // Convert ES6 exports to global assignments
181
- return depContent
182
- .replace(/export\s+const\s+(\w+)\s*=/g, 'window.$1 =')
183
- .replace(/export\s+let\s+(\w+)\s*=/g, 'window.$1 =')
184
- .replace(/export\s+function\s+(\w+)/g, 'window.$1 = function')
185
- .replace(/export\s+default\s+/g, 'window.defaultExport =')
186
- .replace(/export\s*{\s*([^}]+)\s*}/g, (match, exports) => {
187
- return exports.split(',').map(exp => {
188
- const cleanExp = exp.trim();
189
- return `window.${cleanExp} = ${cleanExp};`;
190
- }).join('\n');
191
- });
192
- })
193
- .join('\n\n');
194
-
195
- evalCode = depCode + '\n\n' + evalCode;
196
- }
197
-
198
- // Evaluate the complete code
199
- const componentClass = new Function('slice', 'customElements', 'window', 'document', `
200
- "use strict";
201
- ${evalCode}
202
- return ${componentName};
203
- `)(window.slice, window.customElements, window, window.document);
204
-
205
- if (componentClass) {
206
- this.classes.set(componentName, componentClass);
207
- console.log(`📝 Class registered for: ${componentName}`);
208
- }
209
- } catch (error) {
210
- console.warn(`❌ Failed to evaluate class for ${componentName}:`, error);
211
- console.warn('Code that failed:', componentData.js.substring(0, 200) + '...');
212
- }
213
- }
214
- }
215
- }
216
-
217
-
218
-
219
- /**
220
- * 📦 New bundle registration method (simplified and robust)
221
- */
222
- registerBundle(bundle) {
223
- const { components, metadata } = bundle;
224
-
225
- console.log(`📦 Registering bundle: ${metadata.type} (${metadata.componentCount} components)`);
226
-
227
- // Phase 1: Register all templates and CSS first
228
- for (const [componentName, componentData] of Object.entries(components)) {
229
- try {
230
- if (componentData.html !== undefined && !this.templates.has(componentName)) {
231
- const template = document.createElement('template');
232
- template.innerHTML = componentData.html || '';
233
- this.templates.set(componentName, template);
234
- }
235
-
236
- if (componentData.css !== undefined && !this.requestedStyles.has(componentName)) {
237
- if (window.slice && window.slice.stylesManager) {
238
- window.slice.stylesManager.registerComponentStyles(componentName, componentData.css || '');
239
- this.requestedStyles.add(componentName);
240
- console.log(`🎨 CSS registered for: ${componentName}`);
241
- }
242
- }
243
- } catch (error) {
244
- console.warn(`❌ Failed to register assets for ${componentName}:`, error);
245
- }
246
- }
247
-
248
- // Phase 2: Evaluate all external file dependencies first
249
- const processedDeps = new Set();
250
- for (const [componentName, componentData] of Object.entries(components)) {
251
- if (componentData.externalDependencies) {
252
- for (const [depName, depEntry] of Object.entries(componentData.externalDependencies)) {
253
- const depKey = depName || '';
254
- if (!processedDeps.has(depKey)) {
255
- try {
256
- const depContent = typeof depEntry === 'string' ? depEntry : depEntry.content;
257
- const bindings = typeof depEntry === 'string' ? [] : (depEntry.bindings || []);
258
-
259
- const fileBaseName = depKey
260
- ? depKey.split('/').pop().replace(/\.[^.]+$/, '')
261
- : '';
262
- const dataName = fileBaseName ? `${fileBaseName}Data` : '';
263
- const exportPrefix = dataName ? `window.${dataName} = ` : '';
264
-
265
- // Process ES6 exports to make the code evaluable
266
- let processedContent = depContent
267
- // Convert named exports: export const varName = ... → window.varName = ...
268
- .replace(/export\s+const\s+(\w+)\s*=\s*/g, 'window.$1 = ')
269
- .replace(/export\s+let\s+(\w+)\s*=\s*/g, 'window.$1 = ')
270
- .replace(/export\s+function\s+(\w+)/g, 'window.$1 = function')
271
- .replace(/export\s+default\s+/g, 'window.defaultExport = ')
272
- // Promote default export to <file>Data for data modules
273
- .replace(/window\.defaultExport\s*=\s*/g, exportPrefix || 'window.defaultExport = ')
274
- // Handle export { var1, var2 } statements
275
- .replace(/export\s*{\s*([^}]+)\s*}/g, (match, exportsStr) => {
276
- const exports = exportsStr.split(',').map(exp => exp.trim().split(' as ')[0].trim());
277
- return exports.map(varName => `window.${varName} = ${varName};`).join('\n');
278
- })
279
- // Remove any remaining export keywords
280
- .replace(/^\s*export\s+/gm, '');
281
-
282
- // Evaluate the processed content
283
- new Function('slice', 'customElements', 'window', 'document', processedContent)
284
- (window.slice, window.customElements, window, window.document);
285
-
286
- // Apply import bindings to map local identifiers to globals
287
- for (const binding of bindings) {
288
- if (!binding?.localName) continue;
289
-
290
- if (binding.type === 'default') {
291
- if (!window[binding.localName]) {
292
- const fallbackValue = dataName && window[dataName] !== undefined
293
- ? window[dataName]
294
- : window.defaultExport;
295
- if (fallbackValue !== undefined) {
296
- window[binding.localName] = fallbackValue;
297
- }
298
- }
299
- }
300
-
301
- if (binding.type === 'named') {
302
- if (!window[binding.localName] && window[binding.importedName] !== undefined) {
303
- window[binding.localName] = window[binding.importedName];
304
- }
305
- }
306
-
307
- if (binding.type === 'namespace' && !window[binding.localName]) {
308
- const namespace = {};
309
- Object.keys(window).forEach((key) => {
310
- namespace[key] = window[key];
311
- });
312
- window[binding.localName] = namespace;
313
- }
314
- }
315
-
316
- processedDeps.add(depKey);
317
- console.log(`📄 External dependency loaded: ${depName}`);
318
- } catch (depError) {
319
- console.warn(`⚠️ Failed to load external dependency ${depName}:`, depError);
320
- const preview = typeof depEntry === 'string' ? depEntry : depEntry.content;
321
- console.warn('Original content preview:', preview.substring(0, 200));
322
- }
323
- }
324
- }
325
- }
326
- }
327
-
328
- // Phase 3: Evaluate all component classes (external dependencies are now available)
329
- for (const [componentName, componentData] of Object.entries(components)) {
330
- if (componentData.js && !this.classes.has(componentName)) {
331
- try {
332
- // Simple evaluation
333
- const componentClass = new Function('slice', 'customElements', 'window', 'document', `
334
- ${componentData.js}
335
- return ${componentName};
336
- `)(window.slice, window.customElements, window, window.document);
337
-
338
- if (componentClass) {
339
- this.classes.set(componentName, componentClass);
340
- console.log(`📝 Class registered for: ${componentName}`);
341
- }
342
- } catch (error) {
343
- console.warn(`❌ Failed to evaluate class for ${componentName}:`, error);
344
- // Continue with other components instead of failing completely
345
- }
346
- }
347
- }
348
-
349
- console.log(`✅ Bundle registration completed: ${metadata.componentCount} components processed`);
350
- }
351
-
352
- /**
353
- * 📦 Determines which bundle to load for a component
354
- */
355
- getBundleForComponent(componentName) {
356
- if (!this.bundleConfig?.bundles) return null;
357
-
358
- // Check if component is in critical bundle
359
- if (this.bundleConfig.bundles.critical?.components?.includes(componentName)) {
360
- return 'critical';
361
- }
362
-
363
- // Find component in route bundles
364
- if (this.bundleConfig.bundles.routes) {
365
- for (const [bundleName, bundleInfo] of Object.entries(this.bundleConfig.bundles.routes)) {
366
- if (bundleInfo.components?.includes(componentName)) {
367
- return bundleName;
368
- }
369
- }
370
- }
371
-
372
- return null;
373
- }
374
-
375
- /**
376
- * 📦 Checks if a component is available from loaded bundles
377
- */
378
- isComponentFromBundle(componentName) {
379
- if (!this.bundleConfig?.bundles) return false;
380
-
381
- // Check critical bundle
382
- if (this.bundleConfig.bundles.critical?.components?.includes(componentName)) {
383
- return this.criticalBundleLoaded;
384
- }
385
-
386
- // Check route bundles
387
- if (this.bundleConfig.bundles.routes) {
388
- for (const [bundleName, bundleInfo] of Object.entries(this.bundleConfig.bundles.routes)) {
389
- if (bundleInfo.components?.includes(componentName)) {
390
- return this.loadedBundles.has(bundleName);
391
- }
392
- }
393
- }
394
-
395
- return false;
396
- }
397
-
398
- /**
399
- * 📦 Gets component data from loaded bundles
400
- */
401
- getComponentFromBundle(componentName) {
402
- if (!this.bundleConfig?.bundles) return null;
403
-
404
- // Find component in any loaded bundle
405
- const allBundles = [
406
- { name: 'critical', data: this.bundleConfig.bundles.critical },
407
- ...Object.entries(this.bundleConfig.bundles.routes || {}).map(([name, data]) => ({ name, data }))
408
- ];
409
-
410
- for (const { name: bundleName, data: bundleData } of allBundles) {
411
- if (bundleData?.components?.includes(componentName) && this.loadedBundles.has(bundleName)) {
412
- // Find the bundle file and extract component data
413
- // This is a simplified version - in practice you'd need to access the loaded bundle data
414
- return { bundleName, componentName };
415
- }
416
- }
417
-
418
- return null;
419
- }
420
-
421
- logActiveComponents() {
422
- this.activeComponents.forEach((component) => {
423
- let parent = component.parentComponent;
424
- let parentName = parent ? parent.constructor.name : null;
425
- });
426
- }
427
-
428
- getTopParentsLinkedToActiveComponents() {
429
- let topParentsLinkedToActiveComponents = new Map();
430
- this.activeComponents.forEach((component) => {
431
- let parent = component.parentComponent;
432
- while (parent && parent.parentComponent) {
433
- parent = parent.parentComponent;
434
- }
435
- if (!topParentsLinkedToActiveComponents.has(parent)) {
436
- topParentsLinkedToActiveComponents.set(parent, []);
437
- }
438
- topParentsLinkedToActiveComponents.get(parent).push(component);
439
- });
440
- return topParentsLinkedToActiveComponents;
441
- }
442
-
443
- verifyComponentIds(component) {
444
- const htmlId = component.id;
445
-
446
- if (htmlId && htmlId.trim() !== '') {
447
- if (this.activeComponents.has(htmlId)) {
448
- slice.logger.logError(
449
- 'Controller',
450
- `A component with the same html id attribute is already registered: ${htmlId}`
451
- );
452
- return false;
453
- }
454
- }
455
-
456
- let sliceId = component.sliceId;
457
-
458
- if (sliceId && sliceId.trim() !== '') {
459
- if (this.activeComponents.has(sliceId)) {
460
- slice.logger.logError(
461
- 'Controller',
462
- `A component with the same slice id attribute is already registered: ${sliceId}`
463
- );
464
- return false;
465
- }
466
- } else {
467
- sliceId = `${component.constructor.name[0].toLowerCase()}${component.constructor.name.slice(1)}-${this.idCounter}`;
468
- component.sliceId = sliceId;
469
- this.idCounter++;
470
- }
471
-
472
- component.sliceId = sliceId;
473
- return true;
474
- }
475
-
476
- /**
477
- * Registra un componente y actualiza el índice de relaciones padre-hijo
478
- * 🚀 OPTIMIZADO: Ahora mantiene childrenIndex y precalcula profundidad
479
- */
480
- registerComponent(component, parent = null) {
481
- component.parentComponent = parent;
482
-
483
- // 🚀 OPTIMIZACIÓN: Precalcular y guardar profundidad
484
- component._depth = parent ? (parent._depth || 0) + 1 : 0;
485
-
486
- // Registrar en activeComponents
487
- this.activeComponents.set(component.sliceId, component);
488
-
489
- // 🚀 OPTIMIZACIÓN: Actualizar índice inverso de hijos
490
- if (parent) {
491
- if (!this.childrenIndex.has(parent.sliceId)) {
492
- this.childrenIndex.set(parent.sliceId, new Set());
493
- }
494
- this.childrenIndex.get(parent.sliceId).add(component.sliceId);
495
- }
496
-
497
- return true;
498
- }
499
-
500
- registerComponentsRecursively(component, parent = null) {
501
- // Assign parent if not already set
502
- if (!component.parentComponent) {
503
- component.parentComponent = parent;
504
- }
505
-
506
- // Recursively assign parent to children
507
- component.querySelectorAll('*').forEach((child) => {
508
- if (child.tagName.startsWith('SLICE-')) {
509
- if (!child.parentComponent) {
510
- child.parentComponent = component;
511
- }
512
- this.registerComponentsRecursively(child, component);
513
- }
514
- });
515
- }
516
-
517
- getComponent(sliceId) {
518
- return this.activeComponents.get(sliceId);
519
- }
520
-
521
- loadTemplateToComponent(component) {
522
- const className = component.constructor.name;
523
- const template = this.templates.get(className);
524
-
525
- if (!template) {
526
- slice.logger.logError(`Template not found for component: ${className}`);
527
- return;
528
- }
529
-
530
- component.innerHTML = template.innerHTML;
531
- return component;
532
- }
533
-
534
- getComponentCategory(componentSliceId) {
535
- return this.componentCategories.get(componentSliceId);
536
- }
537
-
538
- async fetchText(componentName, resourceType, componentCategory, customPath) {
539
- try {
540
- const baseUrl = window.location.origin;
541
- let path;
542
-
543
- if (!componentCategory) {
544
- componentCategory = this.componentCategories.get(componentName);
545
- }
546
-
547
- let isVisual = resourceType === 'html' || resourceType === 'css';
548
-
549
- if (isVisual) {
550
- if (slice.paths.components[componentCategory]) {
551
- path = `${baseUrl}${slice.paths.components[componentCategory].path}/${componentName}`;
552
- resourceType === 'html' ? path += `/${componentName}.html` : path += `/${componentName}.css`;
553
- } else {
554
- if (componentCategory === 'Structural') {
555
- path = `${baseUrl}/Slice/Components/Structural/${componentName}`;
556
- resourceType === 'html' ? path += `/${componentName}.html` : path += `/${componentName}.css`;
557
- } else {
558
- throw new Error(`Component category '${componentCategory}' not found in paths configuration`);
559
- }
560
- }
561
- }
562
-
563
- if (resourceType === 'theme') {
564
- path = `${baseUrl}${slice.paths.themes}/${componentName}.css`;
565
- }
566
-
567
- if (resourceType === 'styles') {
568
- path = `${baseUrl}${slice.paths.styles}/${componentName}.css`;
569
- }
570
-
571
- if (customPath) {
572
- path = customPath;
573
- }
574
-
575
- slice.logger.logInfo('Controller', `Fetching ${resourceType} from: ${path}`);
576
-
577
- const response = await fetch(path);
578
-
579
- if (!response.ok) {
580
- throw new Error(`Failed to fetch ${path}: ${response.statusText}`);
581
- }
582
-
583
- const content = await response.text();
584
- slice.logger.logInfo('Controller', `Successfully fetched ${resourceType} for ${componentName}`);
585
- return content;
586
- } catch (error) {
587
- slice.logger.logError('Controller', `Error fetching ${resourceType} for component ${componentName}:`, error);
588
- throw error;
589
- }
590
- }
591
-
592
- setComponentProps(component, props) {
593
- const ComponentClass = component.constructor;
594
- const componentName = ComponentClass.name;
595
-
596
- // Aplicar defaults si tiene static props
597
- if (ComponentClass.props) {
598
- this.applyDefaultProps(component, ComponentClass.props, props);
599
- }
600
-
601
- // Validar solo en desarrollo
602
- if (ComponentClass.props && !slice.isProduction()) {
603
- this.validatePropsInDevelopment(ComponentClass, props, componentName);
604
- }
605
-
606
- // Aplicar props
607
- for (const prop in props) {
608
- component[`_${prop}`] = null;
609
- component[prop] = props[prop];
610
- }
611
- }
612
-
613
- getComponentPropsForDebugger(component) {
614
- const ComponentClass = component.constructor;
615
-
616
- if (ComponentClass.props) {
617
- return {
618
- availableProps: Object.keys(ComponentClass.props),
619
- propsConfig: ComponentClass.props,
620
- usedProps: this.extractUsedProps(component, ComponentClass.props)
621
- };
622
- } else {
623
- return {
624
- availableProps: this.extractUsedProps(component),
625
- propsConfig: null,
626
- usedProps: this.extractUsedProps(component)
627
- };
628
- }
629
- }
630
-
631
- applyDefaultProps(component, staticProps, providedProps) {
632
- Object.entries(staticProps).forEach(([prop, config]) => {
633
- if (config.default !== undefined && !(prop in (providedProps || {}))) {
634
- component[`_${prop}`] = null;
635
- component[prop] = config.default;
636
- }
637
- });
638
- }
639
-
640
- validatePropsInDevelopment(ComponentClass, providedProps, componentName) {
641
- const staticProps = ComponentClass.props;
642
- const usedProps = Object.keys(providedProps || {});
643
-
644
- const availableProps = Object.keys(staticProps);
645
- const unknownProps = usedProps.filter(prop => !availableProps.includes(prop));
646
-
647
- if (unknownProps.length > 0) {
648
- slice.logger.logWarning(
649
- 'PropsValidator',
650
- `${componentName}: Unknown props [${unknownProps.join(', ')}]. Available: [${availableProps.join(', ')}]`
651
- );
652
- }
653
-
654
- const requiredProps = Object.entries(staticProps)
655
- .filter(([_, config]) => config.required)
656
- .map(([prop, _]) => prop);
657
-
658
- const missingRequired = requiredProps.filter(prop => !(prop in (providedProps || {})));
659
- if (missingRequired.length > 0) {
660
- slice.logger.logError(
661
- componentName,
662
- `Missing required props: [${missingRequired.join(', ')}]`
663
- );
664
- }
665
- }
666
-
667
- extractUsedProps(component, staticProps = null) {
668
- const usedProps = {};
669
-
670
- if (staticProps) {
671
- Object.keys(staticProps).forEach(prop => {
672
- if (component[prop] !== undefined) {
673
- usedProps[prop] = component[prop];
674
- }
675
- });
676
- } else {
677
- Object.getOwnPropertyNames(component).forEach(key => {
678
- if (key.startsWith('_') && key !== '_isActive') {
679
- const propName = key.substring(1);
680
- usedProps[propName] = component[propName];
681
- }
682
- });
683
- }
684
-
685
- return usedProps;
686
- }
687
-
688
- // ============================================================================
689
- // 🚀 MÉTODOS DE DESTRUCCIÓN OPTIMIZADOS
690
- // ============================================================================
691
-
692
- /**
693
- * Encuentra recursivamente todos los hijos de un componente
694
- * 🚀 OPTIMIZADO: O(m) en lugar de O(n*d) - usa childrenIndex
695
- * @param {string} parentSliceId - sliceId del componente padre
696
- * @param {Set<string>} collected - Set de sliceIds ya recolectados
697
- * @returns {Set<string>} Set de todos los sliceIds de componentes hijos
698
- */
699
- findAllChildComponents(parentSliceId, collected = new Set()) {
700
- // 🚀 Buscar directamente en el índice: O(1)
701
- const children = this.childrenIndex.get(parentSliceId);
702
-
703
- if (!children) return collected;
704
-
705
- // 🚀 Iterar solo los hijos directos: O(k) donde k = número de hijos
706
- for (const childSliceId of children) {
707
- collected.add(childSliceId);
708
- // Recursión solo sobre hijos, no todos los componentes
709
- this.findAllChildComponents(childSliceId, collected);
710
- }
711
-
712
- return collected;
713
- }
714
-
715
- /**
716
- * Encuentra recursivamente todos los componentes dentro de un contenedor DOM
717
- * Útil para destroyByContainer cuando no tenemos el sliceId del padre
718
- * @param {HTMLElement} container - Elemento contenedor
719
- * @param {Set<string>} collected - Set de sliceIds ya recolectados
720
- * @returns {Set<string>} Set de todos los sliceIds encontrados
721
- */
722
- findAllNestedComponentsInContainer(container, collected = new Set()) {
723
- // Buscar todos los elementos con slice-id en el contenedor
724
- const sliceComponents = container.querySelectorAll('[slice-id]');
725
-
726
- sliceComponents.forEach(element => {
727
- const sliceId = element.getAttribute('slice-id') || element.sliceId;
728
- if (sliceId && this.activeComponents.has(sliceId)) {
729
- collected.add(sliceId);
730
- // 🚀 Usar índice para buscar hijos recursivamente
731
- this.findAllChildComponents(sliceId, collected);
732
- }
733
- });
734
-
735
- return collected;
736
- }
737
-
738
- /**
739
- * Destruye uno o múltiples componentes DE FORMA RECURSIVA
740
- * 🚀 OPTIMIZADO: O(m log m) en lugar de O(n*d + m log m)
741
- * @param {HTMLElement|Array<HTMLElement>|string|Array<string>} components
742
- * @returns {number} Cantidad de componentes destruidos (incluyendo hijos)
743
- */
744
- destroyComponent(components) {
745
- const toDestroy = Array.isArray(components) ? components : [components];
746
- const allSliceIdsToDestroy = new Set();
747
-
748
- // PASO 1: Recolectar todos los componentes padres y sus hijos recursivamente
749
- for (const item of toDestroy) {
750
- let sliceId = null;
751
-
752
- if (typeof item === 'string') {
753
- if (!this.activeComponents.has(item)) {
754
- slice.logger.logWarning('Controller', `Component with sliceId "${item}" not found`);
755
- continue;
756
- }
757
- sliceId = item;
758
- } else if (item && item.sliceId) {
759
- sliceId = item.sliceId;
760
- } else {
761
- slice.logger.logWarning('Controller', `Invalid component or sliceId provided to destroyComponent`);
762
- continue;
763
- }
764
-
765
- allSliceIdsToDestroy.add(sliceId);
766
-
767
- // 🚀 OPTIMIZADO: Usa childrenIndex en lugar de recorrer todos los componentes
768
- this.findAllChildComponents(sliceId, allSliceIdsToDestroy);
769
- }
770
-
771
- // PASO 2: Ordenar por profundidad (más profundos primero)
772
- // 🚀 OPTIMIZADO: Usa _depth precalculada en lugar de calcularla cada vez
773
- const sortedSliceIds = Array.from(allSliceIdsToDestroy).sort((a, b) => {
774
- const compA = this.activeComponents.get(a);
775
- const compB = this.activeComponents.get(b);
776
-
777
- if (!compA || !compB) return 0;
778
-
779
- // 🚀 O(1) en lugar de O(d) - usa profundidad precalculada
780
- return (compB._depth || 0) - (compA._depth || 0);
781
- });
782
-
783
- let destroyedCount = 0;
784
-
785
- // PASO 3: Destruir en orden correcto (hijos antes que padres)
786
- for (const sliceId of sortedSliceIds) {
787
- const component = this.activeComponents.get(sliceId);
788
-
789
- if (!component) continue;
790
-
791
- // Ejecutar hook beforeDestroy si existe
792
- if (typeof component.beforeDestroy === 'function') {
793
- try {
794
- component.beforeDestroy();
795
- } catch (error) {
796
- slice.logger.logError('Controller', `Error in beforeDestroy for ${sliceId}`, error);
797
- }
798
- }
799
-
800
- // 🚀 Limpiar del índice de hijos
801
- this.childrenIndex.delete(sliceId);
802
-
803
- // Si tiene padre, remover de la lista de hijos del padre
804
- if (component.parentComponent) {
805
- const parentChildren = this.childrenIndex.get(component.parentComponent.sliceId);
806
- if (parentChildren) {
807
- parentChildren.delete(sliceId);
808
- // Si el padre no tiene más hijos, eliminar entrada vacía
809
- if (parentChildren.size === 0) {
810
- this.childrenIndex.delete(component.parentComponent.sliceId);
811
- }
812
- }
813
- }
814
-
815
- // Eliminar del mapa de componentes activos
816
- this.activeComponents.delete(sliceId);
817
-
818
- // Remover del DOM si está conectado
819
- if (component.isConnected) {
820
- component.remove();
821
- }
822
-
823
- destroyedCount++;
824
- }
825
-
826
- if (destroyedCount > 0) {
827
- slice.logger.logInfo('Controller', `Destroyed ${destroyedCount} component(s) recursively`);
828
- }
829
-
830
- return destroyedCount;
831
- }
832
-
833
- /**
834
- * Destruye todos los componentes Slice dentro de un contenedor (RECURSIVO)
835
- * 🚀 OPTIMIZADO: Usa el índice inverso para búsqueda rápida
836
- * @param {HTMLElement} container - Elemento contenedor
837
- * @returns {number} Cantidad de componentes destruidos
838
- */
839
- destroyByContainer(container) {
840
- if (!container) {
841
- slice.logger.logWarning('Controller', 'No container provided to destroyByContainer');
842
- return 0;
843
- }
844
-
845
- // 🚀 Recolectar componentes usando índice optimizado
846
- const allSliceIds = this.findAllNestedComponentsInContainer(container);
847
-
848
- if (allSliceIds.size === 0) {
849
- return 0;
850
- }
851
-
852
- // Destruir usando el método principal optimizado
853
- const count = this.destroyComponent(Array.from(allSliceIds));
854
-
855
- if (count > 0) {
856
- slice.logger.logInfo('Controller', `Destroyed ${count} component(s) from container (including nested)`);
857
- }
858
-
859
- return count;
860
- }
861
-
862
- /**
863
- * Destruye componentes cuyos sliceId coincidan con un patrón (RECURSIVO)
864
- * 🚀 OPTIMIZADO: Usa destrucción optimizada
865
- * @param {string|RegExp} pattern - Patrón a buscar
866
- * @returns {number} Cantidad de componentes destruidos
867
- */
868
- destroyByPattern(pattern) {
869
- const componentsToDestroy = [];
870
- const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
871
-
872
- for (const [sliceId, component] of this.activeComponents) {
873
- if (regex.test(sliceId)) {
874
- componentsToDestroy.push(component);
875
- }
876
- }
877
-
878
- if (componentsToDestroy.length === 0) {
879
- return 0;
880
- }
881
-
882
- const count = this.destroyComponent(componentsToDestroy);
883
-
884
- if (count > 0) {
885
- slice.logger.logInfo('Controller', `Destroyed ${count} component(s) matching pattern: ${pattern} (including nested)`);
886
- }
887
-
888
- return count;
889
- }
890
- }
1
+ import components from '/Components/components.js';
2
+
3
+ export default class Controller {
4
+ constructor() {
5
+ this.componentCategories = new Map(Object.entries(components));
6
+ this.templates = new Map();
7
+ this.classes = new Map();
8
+ this.requestedStyles = new Set(); // ✅ CRÍTICO: Para tracking de CSS cargados
9
+ this.activeComponents = new Map();
10
+
11
+ // 🚀 OPTIMIZACIÓN: Índice inverso para búsqueda rápida de hijos
12
+ // parentSliceId → Set<childSliceId>
13
+ this.childrenIndex = new Map();
14
+
15
+ // 📦 Bundle system
16
+ this.loadedBundles = new Set();
17
+ this.bundleConfig = null;
18
+ this.criticalBundleLoaded = false;
19
+
20
+ this.idCounter = 0;
21
+ }
22
+
23
+ /**
24
+ * 📦 Initializes bundle system (called automatically when config is loaded)
25
+ */
26
+ initializeBundles(config = null) {
27
+ if (config) {
28
+ this.bundleConfig = config;
29
+
30
+ // Register critical bundle components if available
31
+ if (config.bundles?.critical) {
32
+ // The critical bundle should already be loaded, register its components
33
+ this.loadedBundles.add('critical');
34
+ // Note: Critical bundle registration is handled by the auto-import
35
+ }
36
+ this.criticalBundleLoaded = true;
37
+ } else {
38
+ // No bundles available, will use individual component loading
39
+ this.bundleConfig = null;
40
+ this.criticalBundleLoaded = false;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * 📦 Loads a bundle by name or category
46
+ */
47
+ async loadBundle(bundleName) {
48
+ if (this.loadedBundles.has(bundleName)) {
49
+ return; // Already loaded
50
+ }
51
+
52
+ try {
53
+ let bundleInfo = this.bundleConfig?.bundles?.routes?.[bundleName];
54
+
55
+ if (!bundleInfo && this.bundleConfig?.bundles?.routes) {
56
+ const normalizedName = bundleName?.toLowerCase();
57
+ const matchedKey = Object.keys(this.bundleConfig.bundles.routes).find(
58
+ (key) => key.toLowerCase() === normalizedName
59
+ );
60
+ if (matchedKey) {
61
+ bundleInfo = this.bundleConfig.bundles.routes[matchedKey];
62
+ }
63
+ }
64
+
65
+ if (!bundleInfo) {
66
+ console.warn(`Bundle ${bundleName} not found in configuration`);
67
+ return;
68
+ }
69
+
70
+ const bundlePath = `/bundles/${bundleInfo.file}`;
71
+
72
+ // Dynamic import of the bundle
73
+ const bundleModule = await import(bundlePath);
74
+
75
+ // Manually register components from the imported bundle
76
+ if (bundleModule.SLICE_BUNDLE) {
77
+ this.registerBundle(bundleModule.SLICE_BUNDLE);
78
+ }
79
+
80
+ this.loadedBundles.add(bundleName);
81
+ } catch (error) {
82
+ console.warn(`Failed to load bundle ${bundleName}:`, error);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 📦 Registers a bundle's components (called automatically by bundle files)
88
+ */
89
+ registerBundleLegacy(bundle) {
90
+ const { components, metadata } = bundle;
91
+
92
+ console.log(`📦 Registering bundle: ${metadata.type} (${metadata.componentCount} components)`);
93
+
94
+ // Phase 1: Register templates and CSS for all components first
95
+ for (const [componentName, componentData] of Object.entries(components)) {
96
+ try {
97
+ // Register HTML template
98
+ if (componentData.html !== undefined && !this.templates.has(componentName)) {
99
+ const template = document.createElement('template');
100
+ template.innerHTML = componentData.html || '';
101
+ this.templates.set(componentName, template);
102
+ }
103
+
104
+ // Register CSS styles
105
+ if (componentData.css !== undefined && !this.requestedStyles.has(componentName)) {
106
+ // Use the existing stylesManager to register component styles
107
+ if (window.slice && window.slice.stylesManager) {
108
+ window.slice.stylesManager.registerComponentStyles(componentName, componentData.css || '');
109
+ this.requestedStyles.add(componentName);
110
+ }
111
+ }
112
+ } catch (error) {
113
+ console.warn(`❌ Failed to register assets for ${componentName}:`, error);
114
+ }
115
+ }
116
+
117
+ // Phase 2: Evaluate all external file dependencies
118
+ const processedDeps = new Set();
119
+ for (const [componentName, componentData] of Object.entries(components)) {
120
+ if (componentData.dependencies) {
121
+ for (const [depName, depContent] of Object.entries(componentData.dependencies)) {
122
+ if (!processedDeps.has(depName)) {
123
+ try {
124
+ // Convert ES6 exports to global assignments
125
+ let processedContent = depContent
126
+ .replace(/export\s+const\s+(\w+)\s*=/g, 'window.$1 =')
127
+ .replace(/export\s+let\s+(\w+)\s*=/g, 'window.$1 =')
128
+ .replace(/export\s+var\s+(\w+)\s*=/g, 'window.$1 =')
129
+ .replace(/export\s+function\s+(\w+)/g, 'window.$1 = function')
130
+ .replace(/export\s+default\s+/g, 'window.defaultExport =')
131
+ .replace(/export\s*{\s*([^}]+)\s*}/g, (match, exports) => {
132
+ return exports
133
+ .split(',')
134
+ .map((exp) => {
135
+ const cleanExp = exp.trim();
136
+ const varName = cleanExp.split(' as ')[0].trim();
137
+ return `window.${varName} = ${varName};`;
138
+ })
139
+ .join('\n');
140
+ })
141
+ // Remove any remaining export keywords
142
+ .replace(/^\s*export\s+/gm, '');
143
+
144
+ // Evaluate the dependency
145
+ try {
146
+ new Function('slice', 'customElements', 'window', 'document', processedContent)(
147
+ window.slice,
148
+ window.customElements,
149
+ window,
150
+ window.document
151
+ );
152
+ } catch (evalError) {
153
+ console.warn(`❌ Failed to evaluate processed dependency ${depName}:`, evalError);
154
+ console.warn('Processed content preview:', processedContent.substring(0, 200));
155
+ // Try evaluating the original content as fallback
156
+ try {
157
+ new Function('slice', 'customElements', 'window', 'document', depContent)(
158
+ window.slice,
159
+ window.customElements,
160
+ window,
161
+ window.document
162
+ );
163
+ console.log(`✅ Fallback evaluation succeeded for ${depName}`);
164
+ } catch (fallbackError) {
165
+ console.warn(`❌ Fallback evaluation also failed for ${depName}:`, fallbackError);
166
+ }
167
+ }
168
+
169
+ processedDeps.add(depName);
170
+ console.log(`📄 Dependency loaded: ${depName}`);
171
+ } catch (depError) {
172
+ console.warn(`⚠️ Failed to load dependency ${depName} for ${componentName}:`, depError);
173
+ }
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ // Phase 3: Evaluate all component classes (now that dependencies are available)
180
+ for (const [componentName, componentData] of Object.entries(components)) {
181
+ // For JavaScript classes, we need to evaluate the code
182
+ if (componentData.js && !this.classes.has(componentName)) {
183
+ try {
184
+ // Create evaluation context with dependencies
185
+ let evalCode = componentData.js;
186
+
187
+ // Prepend dependencies to make them available
188
+ if (componentData.dependencies) {
189
+ const depCode = Object.entries(componentData.dependencies)
190
+ .map(([depName, depContent]) => {
191
+ // Convert ES6 exports to global assignments
192
+ return depContent
193
+ .replace(/export\s+const\s+(\w+)\s*=/g, 'window.$1 =')
194
+ .replace(/export\s+let\s+(\w+)\s*=/g, 'window.$1 =')
195
+ .replace(/export\s+function\s+(\w+)/g, 'window.$1 = function')
196
+ .replace(/export\s+default\s+/g, 'window.defaultExport =')
197
+ .replace(/export\s*{\s*([^}]+)\s*}/g, (match, exports) => {
198
+ return exports
199
+ .split(',')
200
+ .map((exp) => {
201
+ const cleanExp = exp.trim();
202
+ return `window.${cleanExp} = ${cleanExp};`;
203
+ })
204
+ .join('\n');
205
+ });
206
+ })
207
+ .join('\n\n');
208
+
209
+ evalCode = depCode + '\n\n' + evalCode;
210
+ }
211
+
212
+ // Evaluate the complete code
213
+ const componentClass = new Function(
214
+ 'slice',
215
+ 'customElements',
216
+ 'window',
217
+ 'document',
218
+ `
219
+ "use strict";
220
+ ${evalCode}
221
+ return ${componentName};
222
+ `
223
+ )(window.slice, window.customElements, window, window.document);
224
+
225
+ if (componentClass) {
226
+ this.classes.set(componentName, componentClass);
227
+ console.log(`📝 Class registered for: ${componentName}`);
228
+ }
229
+ } catch (error) {
230
+ console.warn(`❌ Failed to evaluate class for ${componentName}:`, error);
231
+ console.warn('Code that failed:', componentData.js.substring(0, 200) + '...');
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ /**
238
+ * 📦 New bundle registration method (simplified and robust)
239
+ */
240
+ registerBundle(bundle) {
241
+ const { components, metadata } = bundle;
242
+
243
+ console.log(`📦 Registering bundle: ${metadata.type} (${metadata.componentCount} components)`);
244
+
245
+ // Phase 1: Register all templates and CSS first
246
+ for (const [componentName, componentData] of Object.entries(components)) {
247
+ try {
248
+ if (componentData.html !== undefined && !this.templates.has(componentName)) {
249
+ const template = document.createElement('template');
250
+ template.innerHTML = componentData.html || '';
251
+ this.templates.set(componentName, template);
252
+ }
253
+
254
+ if (componentData.css !== undefined && !this.requestedStyles.has(componentName)) {
255
+ if (window.slice && window.slice.stylesManager) {
256
+ window.slice.stylesManager.registerComponentStyles(componentName, componentData.css || '');
257
+ this.requestedStyles.add(componentName);
258
+ console.log(`🎨 CSS registered for: ${componentName}`);
259
+ }
260
+ }
261
+ } catch (error) {
262
+ console.warn(`❌ Failed to register assets for ${componentName}:`, error);
263
+ }
264
+ }
265
+
266
+ // Phase 2: Evaluate all external file dependencies first
267
+ const processedDeps = new Set();
268
+ for (const [componentName, componentData] of Object.entries(components)) {
269
+ if (componentData.externalDependencies) {
270
+ for (const [depName, depEntry] of Object.entries(componentData.externalDependencies)) {
271
+ const depKey = depName || '';
272
+ if (!processedDeps.has(depKey)) {
273
+ try {
274
+ const depContent = typeof depEntry === 'string' ? depEntry : depEntry.content;
275
+ const bindings = typeof depEntry === 'string' ? [] : depEntry.bindings || [];
276
+
277
+ const fileBaseName = depKey
278
+ ? depKey
279
+ .split('/')
280
+ .pop()
281
+ .replace(/\.[^.]+$/, '')
282
+ : '';
283
+ const dataName = fileBaseName ? `${fileBaseName}Data` : '';
284
+ const exportPrefix = dataName ? `window.${dataName} = ` : '';
285
+
286
+ // Process ES6 exports to make the code evaluable
287
+ let processedContent = depContent
288
+ // Convert named exports: export const varName = ... → window.varName = ...
289
+ .replace(/export\s+const\s+(\w+)\s*=\s*/g, 'window.$1 = ')
290
+ .replace(/export\s+let\s+(\w+)\s*=\s*/g, 'window.$1 = ')
291
+ .replace(/export\s+function\s+(\w+)/g, 'window.$1 = function')
292
+ .replace(/export\s+default\s+/g, 'window.defaultExport = ')
293
+ // Promote default export to <file>Data for data modules
294
+ .replace(/window\.defaultExport\s*=\s*/g, exportPrefix || 'window.defaultExport = ')
295
+ // Handle export { var1, var2 } statements
296
+ .replace(/export\s*{\s*([^}]+)\s*}/g, (match, exportsStr) => {
297
+ const exports = exportsStr.split(',').map((exp) => exp.trim().split(' as ')[0].trim());
298
+ return exports.map((varName) => `window.${varName} = ${varName};`).join('\n');
299
+ })
300
+ // Remove any remaining export keywords
301
+ .replace(/^\s*export\s+/gm, '');
302
+
303
+ // Evaluate the processed content
304
+ new Function('slice', 'customElements', 'window', 'document', processedContent)(
305
+ window.slice,
306
+ window.customElements,
307
+ window,
308
+ window.document
309
+ );
310
+
311
+ // Apply import bindings to map local identifiers to globals
312
+ for (const binding of bindings) {
313
+ if (!binding?.localName) continue;
314
+
315
+ if (binding.type === 'default') {
316
+ if (!window[binding.localName]) {
317
+ const fallbackValue =
318
+ dataName && window[dataName] !== undefined ? window[dataName] : window.defaultExport;
319
+ if (fallbackValue !== undefined) {
320
+ window[binding.localName] = fallbackValue;
321
+ }
322
+ }
323
+ }
324
+
325
+ if (binding.type === 'named') {
326
+ if (!window[binding.localName] && window[binding.importedName] !== undefined) {
327
+ window[binding.localName] = window[binding.importedName];
328
+ }
329
+ }
330
+
331
+ if (binding.type === 'namespace' && !window[binding.localName]) {
332
+ const namespace = {};
333
+ Object.keys(window).forEach((key) => {
334
+ namespace[key] = window[key];
335
+ });
336
+ window[binding.localName] = namespace;
337
+ }
338
+ }
339
+
340
+ processedDeps.add(depKey);
341
+ console.log(`📄 External dependency loaded: ${depName}`);
342
+ } catch (depError) {
343
+ console.warn(`⚠️ Failed to load external dependency ${depName}:`, depError);
344
+ const preview = typeof depEntry === 'string' ? depEntry : depEntry.content;
345
+ console.warn('Original content preview:', preview.substring(0, 200));
346
+ }
347
+ }
348
+ }
349
+ }
350
+ }
351
+
352
+ // Phase 3: Evaluate all component classes (external dependencies are now available)
353
+ for (const [componentName, componentData] of Object.entries(components)) {
354
+ if (componentData.js && !this.classes.has(componentName)) {
355
+ try {
356
+ // Simple evaluation
357
+ const componentClass = new Function(
358
+ 'slice',
359
+ 'customElements',
360
+ 'window',
361
+ 'document',
362
+ `
363
+ ${componentData.js}
364
+ return ${componentName};
365
+ `
366
+ )(window.slice, window.customElements, window, window.document);
367
+
368
+ if (componentClass) {
369
+ this.classes.set(componentName, componentClass);
370
+ console.log(`📝 Class registered for: ${componentName}`);
371
+ }
372
+ } catch (error) {
373
+ console.warn(`❌ Failed to evaluate class for ${componentName}:`, error);
374
+ // Continue with other components instead of failing completely
375
+ }
376
+ }
377
+ }
378
+
379
+ console.log(`✅ Bundle registration completed: ${metadata.componentCount} components processed`);
380
+ }
381
+
382
+ /**
383
+ * 📦 Determines which bundle to load for a component
384
+ */
385
+ getBundleForComponent(componentName) {
386
+ if (!this.bundleConfig?.bundles) return null;
387
+
388
+ // Check if component is in critical bundle
389
+ if (this.bundleConfig.bundles.critical?.components?.includes(componentName)) {
390
+ return 'critical';
391
+ }
392
+
393
+ // Find component in route bundles
394
+ if (this.bundleConfig.bundles.routes) {
395
+ for (const [bundleName, bundleInfo] of Object.entries(this.bundleConfig.bundles.routes)) {
396
+ if (bundleInfo.components?.includes(componentName)) {
397
+ return bundleName;
398
+ }
399
+ }
400
+ }
401
+
402
+ return null;
403
+ }
404
+
405
+ /**
406
+ * 📦 Checks if a component is available from loaded bundles
407
+ */
408
+ isComponentFromBundle(componentName) {
409
+ if (!this.bundleConfig?.bundles) return false;
410
+
411
+ // Check critical bundle
412
+ if (this.bundleConfig.bundles.critical?.components?.includes(componentName)) {
413
+ return this.criticalBundleLoaded;
414
+ }
415
+
416
+ // Check route bundles
417
+ if (this.bundleConfig.bundles.routes) {
418
+ for (const [bundleName, bundleInfo] of Object.entries(this.bundleConfig.bundles.routes)) {
419
+ if (bundleInfo.components?.includes(componentName)) {
420
+ return this.loadedBundles.has(bundleName);
421
+ }
422
+ }
423
+ }
424
+
425
+ return false;
426
+ }
427
+
428
+ /**
429
+ * 📦 Gets component data from loaded bundles
430
+ */
431
+ getComponentFromBundle(componentName) {
432
+ if (!this.bundleConfig?.bundles) return null;
433
+
434
+ // Find component in any loaded bundle
435
+ const allBundles = [
436
+ { name: 'critical', data: this.bundleConfig.bundles.critical },
437
+ ...Object.entries(this.bundleConfig.bundles.routes || {}).map(([name, data]) => ({ name, data })),
438
+ ];
439
+
440
+ for (const { name: bundleName, data: bundleData } of allBundles) {
441
+ if (bundleData?.components?.includes(componentName) && this.loadedBundles.has(bundleName)) {
442
+ // Find the bundle file and extract component data
443
+ // This is a simplified version - in practice you'd need to access the loaded bundle data
444
+ return { bundleName, componentName };
445
+ }
446
+ }
447
+
448
+ return null;
449
+ }
450
+
451
+ logActiveComponents() {
452
+ this.activeComponents.forEach((component) => {
453
+ let parent = component.parentComponent;
454
+ let parentName = parent ? parent.constructor.name : null;
455
+ });
456
+ }
457
+
458
+ getTopParentsLinkedToActiveComponents() {
459
+ let topParentsLinkedToActiveComponents = new Map();
460
+ this.activeComponents.forEach((component) => {
461
+ let parent = component.parentComponent;
462
+ while (parent && parent.parentComponent) {
463
+ parent = parent.parentComponent;
464
+ }
465
+ if (!topParentsLinkedToActiveComponents.has(parent)) {
466
+ topParentsLinkedToActiveComponents.set(parent, []);
467
+ }
468
+ topParentsLinkedToActiveComponents.get(parent).push(component);
469
+ });
470
+ return topParentsLinkedToActiveComponents;
471
+ }
472
+
473
+ verifyComponentIds(component) {
474
+ const htmlId = component.id;
475
+
476
+ if (htmlId && htmlId.trim() !== '') {
477
+ if (this.activeComponents.has(htmlId)) {
478
+ slice.logger.logError(
479
+ 'Controller',
480
+ `A component with the same html id attribute is already registered: ${htmlId}`
481
+ );
482
+ return false;
483
+ }
484
+ }
485
+
486
+ let sliceId = component.sliceId;
487
+
488
+ if (sliceId && sliceId.trim() !== '') {
489
+ if (this.activeComponents.has(sliceId)) {
490
+ slice.logger.logError(
491
+ 'Controller',
492
+ `A component with the same slice id attribute is already registered: ${sliceId}`
493
+ );
494
+ return false;
495
+ }
496
+ } else {
497
+ sliceId = `${component.constructor.name[0].toLowerCase()}${component.constructor.name.slice(1)}-${this.idCounter}`;
498
+ component.sliceId = sliceId;
499
+ this.idCounter++;
500
+ }
501
+
502
+ component.sliceId = sliceId;
503
+ return true;
504
+ }
505
+
506
+ /**
507
+ * Registra un componente y actualiza el índice de relaciones padre-hijo
508
+ * 🚀 OPTIMIZADO: Ahora mantiene childrenIndex y precalcula profundidad
509
+ */
510
+ registerComponent(component, parent = null) {
511
+ component.parentComponent = parent;
512
+
513
+ // 🚀 OPTIMIZACIÓN: Precalcular y guardar profundidad
514
+ component._depth = parent ? (parent._depth || 0) + 1 : 0;
515
+
516
+ // Registrar en activeComponents
517
+ this.activeComponents.set(component.sliceId, component);
518
+
519
+ // 🚀 OPTIMIZACIÓN: Actualizar índice inverso de hijos
520
+ if (parent) {
521
+ if (!this.childrenIndex.has(parent.sliceId)) {
522
+ this.childrenIndex.set(parent.sliceId, new Set());
523
+ }
524
+ this.childrenIndex.get(parent.sliceId).add(component.sliceId);
525
+ }
526
+
527
+ return true;
528
+ }
529
+
530
+ registerComponentsRecursively(component, parent = null) {
531
+ // Assign parent if not already set
532
+ if (!component.parentComponent) {
533
+ component.parentComponent = parent;
534
+ }
535
+
536
+ // Recursively assign parent to children
537
+ component.querySelectorAll('*').forEach((child) => {
538
+ if (child.tagName.startsWith('SLICE-')) {
539
+ if (!child.parentComponent) {
540
+ child.parentComponent = component;
541
+ }
542
+ this.registerComponentsRecursively(child, component);
543
+ }
544
+ });
545
+ }
546
+
547
+ getComponent(sliceId) {
548
+ return this.activeComponents.get(sliceId);
549
+ }
550
+
551
+ loadTemplateToComponent(component) {
552
+ const className = component.constructor.name;
553
+ const template = this.templates.get(className);
554
+
555
+ if (!template) {
556
+ slice.logger.logError(`Template not found for component: ${className}`);
557
+ return;
558
+ }
559
+
560
+ component.innerHTML = template.innerHTML;
561
+ return component;
562
+ }
563
+
564
+ getComponentCategory(componentSliceId) {
565
+ return this.componentCategories.get(componentSliceId);
566
+ }
567
+
568
+ async fetchText(componentName, resourceType, componentCategory, customPath) {
569
+ try {
570
+ const baseUrl = window.location.origin;
571
+ let path;
572
+
573
+ if (!componentCategory) {
574
+ componentCategory = this.componentCategories.get(componentName);
575
+ }
576
+
577
+ let isVisual = resourceType === 'html' || resourceType === 'css';
578
+
579
+ if (isVisual) {
580
+ if (slice.paths.components[componentCategory]) {
581
+ path = `${baseUrl}${slice.paths.components[componentCategory].path}/${componentName}`;
582
+ resourceType === 'html' ? (path += `/${componentName}.html`) : (path += `/${componentName}.css`);
583
+ } else {
584
+ if (componentCategory === 'Structural') {
585
+ path = `${baseUrl}/Slice/Components/Structural/${componentName}`;
586
+ resourceType === 'html' ? (path += `/${componentName}.html`) : (path += `/${componentName}.css`);
587
+ } else {
588
+ throw new Error(`Component category '${componentCategory}' not found in paths configuration`);
589
+ }
590
+ }
591
+ }
592
+
593
+ if (resourceType === 'theme') {
594
+ path = `${baseUrl}${slice.paths.themes}/${componentName}.css`;
595
+ }
596
+
597
+ if (resourceType === 'styles') {
598
+ path = `${baseUrl}${slice.paths.styles}/${componentName}.css`;
599
+ }
600
+
601
+ if (customPath) {
602
+ path = customPath;
603
+ }
604
+
605
+ slice.logger.logInfo('Controller', `Fetching ${resourceType} from: ${path}`);
606
+
607
+ const response = await fetch(path);
608
+
609
+ if (!response.ok) {
610
+ throw new Error(`Failed to fetch ${path}: ${response.statusText}`);
611
+ }
612
+
613
+ const content = await response.text();
614
+ slice.logger.logInfo('Controller', `Successfully fetched ${resourceType} for ${componentName}`);
615
+ return content;
616
+ } catch (error) {
617
+ slice.logger.logError('Controller', `Error fetching ${resourceType} for component ${componentName}:`, error);
618
+ throw error;
619
+ }
620
+ }
621
+
622
+ setComponentProps(component, props) {
623
+ const ComponentClass = component.constructor;
624
+ const componentName = ComponentClass.name;
625
+
626
+ // Aplicar defaults si tiene static props
627
+ if (ComponentClass.props) {
628
+ this.applyDefaultProps(component, ComponentClass.props, props);
629
+ }
630
+
631
+ // Validar solo en desarrollo
632
+ if (ComponentClass.props && !slice.isProduction()) {
633
+ this.validatePropsInDevelopment(ComponentClass, props, componentName);
634
+ }
635
+
636
+ // Aplicar props
637
+ for (const prop in props) {
638
+ component[`_${prop}`] = null;
639
+ component[prop] = props[prop];
640
+ }
641
+ }
642
+
643
+ getComponentPropsForDebugger(component) {
644
+ const ComponentClass = component.constructor;
645
+
646
+ if (ComponentClass.props) {
647
+ return {
648
+ availableProps: Object.keys(ComponentClass.props),
649
+ propsConfig: ComponentClass.props,
650
+ usedProps: this.extractUsedProps(component, ComponentClass.props),
651
+ };
652
+ } else {
653
+ return {
654
+ availableProps: this.extractUsedProps(component),
655
+ propsConfig: null,
656
+ usedProps: this.extractUsedProps(component),
657
+ };
658
+ }
659
+ }
660
+
661
+ applyDefaultProps(component, staticProps, providedProps) {
662
+ Object.entries(staticProps).forEach(([prop, config]) => {
663
+ if (config.default !== undefined && !(prop in (providedProps || {}))) {
664
+ component[`_${prop}`] = null;
665
+ component[prop] = config.default;
666
+ }
667
+ });
668
+ }
669
+
670
+ validatePropsInDevelopment(ComponentClass, providedProps, componentName) {
671
+ const staticProps = ComponentClass.props;
672
+ const usedProps = Object.keys(providedProps || {});
673
+
674
+ const availableProps = Object.keys(staticProps);
675
+ const unknownProps = usedProps.filter((prop) => !availableProps.includes(prop));
676
+
677
+ if (unknownProps.length > 0) {
678
+ slice.logger.logWarning(
679
+ 'PropsValidator',
680
+ `${componentName}: Unknown props [${unknownProps.join(', ')}]. Available: [${availableProps.join(', ')}]`
681
+ );
682
+ }
683
+
684
+ const requiredProps = Object.entries(staticProps)
685
+ .filter(([_, config]) => config.required)
686
+ .map(([prop, _]) => prop);
687
+
688
+ const missingRequired = requiredProps.filter((prop) => !(prop in (providedProps || {})));
689
+ if (missingRequired.length > 0) {
690
+ slice.logger.logError(componentName, `Missing required props: [${missingRequired.join(', ')}]`);
691
+ }
692
+ }
693
+
694
+ extractUsedProps(component, staticProps = null) {
695
+ const usedProps = {};
696
+
697
+ if (staticProps) {
698
+ Object.keys(staticProps).forEach((prop) => {
699
+ if (component[prop] !== undefined) {
700
+ usedProps[prop] = component[prop];
701
+ }
702
+ });
703
+ } else {
704
+ Object.getOwnPropertyNames(component).forEach((key) => {
705
+ if (key.startsWith('_') && key !== '_isActive') {
706
+ const propName = key.substring(1);
707
+ usedProps[propName] = component[propName];
708
+ }
709
+ });
710
+ }
711
+
712
+ return usedProps;
713
+ }
714
+
715
+ // ============================================================================
716
+ // 🚀 MÉTODOS DE DESTRUCCIÓN OPTIMIZADOS
717
+ // ============================================================================
718
+
719
+ /**
720
+ * Encuentra recursivamente todos los hijos de un componente
721
+ * 🚀 OPTIMIZADO: O(m) en lugar de O(n*d) - usa childrenIndex
722
+ * @param {string} parentSliceId - sliceId del componente padre
723
+ * @param {Set<string>} collected - Set de sliceIds ya recolectados
724
+ * @returns {Set<string>} Set de todos los sliceIds de componentes hijos
725
+ */
726
+ findAllChildComponents(parentSliceId, collected = new Set()) {
727
+ // 🚀 Buscar directamente en el índice: O(1)
728
+ const children = this.childrenIndex.get(parentSliceId);
729
+
730
+ if (!children) return collected;
731
+
732
+ // 🚀 Iterar solo los hijos directos: O(k) donde k = número de hijos
733
+ for (const childSliceId of children) {
734
+ collected.add(childSliceId);
735
+ // Recursión solo sobre hijos, no todos los componentes
736
+ this.findAllChildComponents(childSliceId, collected);
737
+ }
738
+
739
+ return collected;
740
+ }
741
+
742
+ /**
743
+ * Encuentra recursivamente todos los componentes dentro de un contenedor DOM
744
+ * Útil para destroyByContainer cuando no tenemos el sliceId del padre
745
+ * @param {HTMLElement} container - Elemento contenedor
746
+ * @param {Set<string>} collected - Set de sliceIds ya recolectados
747
+ * @returns {Set<string>} Set de todos los sliceIds encontrados
748
+ */
749
+ findAllNestedComponentsInContainer(container, collected = new Set()) {
750
+ // Buscar todos los elementos con slice-id en el contenedor
751
+ const sliceComponents = container.querySelectorAll('[slice-id]');
752
+
753
+ sliceComponents.forEach((element) => {
754
+ const sliceId = element.getAttribute('slice-id') || element.sliceId;
755
+ if (sliceId && this.activeComponents.has(sliceId)) {
756
+ collected.add(sliceId);
757
+ // 🚀 Usar índice para buscar hijos recursivamente
758
+ this.findAllChildComponents(sliceId, collected);
759
+ }
760
+ });
761
+
762
+ return collected;
763
+ }
764
+
765
+ /**
766
+ * Destruye uno o múltiples componentes DE FORMA RECURSIVA
767
+ * 🚀 OPTIMIZADO: O(m log m) en lugar de O(n*d + m log m)
768
+ * @param {HTMLElement|Array<HTMLElement>|string|Array<string>} components
769
+ * @returns {number} Cantidad de componentes destruidos (incluyendo hijos)
770
+ */
771
+ destroyComponent(components) {
772
+ const toDestroy = Array.isArray(components) ? components : [components];
773
+ const allSliceIdsToDestroy = new Set();
774
+
775
+ // PASO 1: Recolectar todos los componentes padres y sus hijos recursivamente
776
+ for (const item of toDestroy) {
777
+ let sliceId = null;
778
+
779
+ if (typeof item === 'string') {
780
+ if (!this.activeComponents.has(item)) {
781
+ slice.logger.logWarning('Controller', `Component with sliceId "${item}" not found`);
782
+ continue;
783
+ }
784
+ sliceId = item;
785
+ } else if (item && item.sliceId) {
786
+ sliceId = item.sliceId;
787
+ } else {
788
+ slice.logger.logWarning('Controller', `Invalid component or sliceId provided to destroyComponent`);
789
+ continue;
790
+ }
791
+
792
+ allSliceIdsToDestroy.add(sliceId);
793
+
794
+ // 🚀 OPTIMIZADO: Usa childrenIndex en lugar de recorrer todos los componentes
795
+ this.findAllChildComponents(sliceId, allSliceIdsToDestroy);
796
+ }
797
+
798
+ // PASO 2: Ordenar por profundidad (más profundos primero)
799
+ // 🚀 OPTIMIZADO: Usa _depth precalculada en lugar de calcularla cada vez
800
+ const sortedSliceIds = Array.from(allSliceIdsToDestroy).sort((a, b) => {
801
+ const compA = this.activeComponents.get(a);
802
+ const compB = this.activeComponents.get(b);
803
+
804
+ if (!compA || !compB) return 0;
805
+
806
+ // 🚀 O(1) en lugar de O(d) - usa profundidad precalculada
807
+ return (compB._depth || 0) - (compA._depth || 0);
808
+ });
809
+
810
+ let destroyedCount = 0;
811
+
812
+ // PASO 3: Destruir en orden correcto (hijos antes que padres)
813
+ for (const sliceId of sortedSliceIds) {
814
+ const component = this.activeComponents.get(sliceId);
815
+
816
+ if (!component) continue;
817
+
818
+ // Ejecutar hook beforeDestroy si existe
819
+ if (typeof component.beforeDestroy === 'function') {
820
+ try {
821
+ component.beforeDestroy();
822
+ } catch (error) {
823
+ slice.logger.logError('Controller', `Error in beforeDestroy for ${sliceId}`, error);
824
+ }
825
+ }
826
+
827
+ // Limpiar suscripciones de eventos del componente
828
+ if (slice.events) {
829
+ slice.events.cleanupComponent(sliceId);
830
+ }
831
+
832
+ // 🚀 Limpiar del índice de hijos
833
+ this.childrenIndex.delete(sliceId);
834
+
835
+ // Si tiene padre, remover de la lista de hijos del padre
836
+ if (component.parentComponent) {
837
+ const parentChildren = this.childrenIndex.get(component.parentComponent.sliceId);
838
+ if (parentChildren) {
839
+ parentChildren.delete(sliceId);
840
+ // Si el padre no tiene más hijos, eliminar entrada vacía
841
+ if (parentChildren.size === 0) {
842
+ this.childrenIndex.delete(component.parentComponent.sliceId);
843
+ }
844
+ }
845
+ }
846
+
847
+ // Eliminar del mapa de componentes activos
848
+ this.activeComponents.delete(sliceId);
849
+
850
+ // Remover del DOM si está conectado
851
+ if (component.isConnected) {
852
+ component.remove();
853
+ }
854
+
855
+ destroyedCount++;
856
+ }
857
+
858
+ if (destroyedCount > 0) {
859
+ slice.logger.logInfo('Controller', `Destroyed ${destroyedCount} component(s) recursively`);
860
+ }
861
+
862
+ return destroyedCount;
863
+ }
864
+
865
+ /**
866
+ * Destruye todos los componentes Slice dentro de un contenedor (RECURSIVO)
867
+ * 🚀 OPTIMIZADO: Usa el índice inverso para búsqueda rápida
868
+ * @param {HTMLElement} container - Elemento contenedor
869
+ * @returns {number} Cantidad de componentes destruidos
870
+ */
871
+ destroyByContainer(container) {
872
+ if (!container) {
873
+ slice.logger.logWarning('Controller', 'No container provided to destroyByContainer');
874
+ return 0;
875
+ }
876
+
877
+ // 🚀 Recolectar componentes usando índice optimizado
878
+ const allSliceIds = this.findAllNestedComponentsInContainer(container);
879
+
880
+ if (allSliceIds.size === 0) {
881
+ return 0;
882
+ }
883
+
884
+ // Destruir usando el método principal optimizado
885
+ const count = this.destroyComponent(Array.from(allSliceIds));
886
+
887
+ if (count > 0) {
888
+ slice.logger.logInfo('Controller', `Destroyed ${count} component(s) from container (including nested)`);
889
+ }
890
+
891
+ return count;
892
+ }
893
+
894
+ /**
895
+ * Destruye componentes cuyos sliceId coincidan con un patrón (RECURSIVO)
896
+ * 🚀 OPTIMIZADO: Usa destrucción optimizada
897
+ * @param {string|RegExp} pattern - Patrón a buscar
898
+ * @returns {number} Cantidad de componentes destruidos
899
+ */
900
+ destroyByPattern(pattern) {
901
+ const componentsToDestroy = [];
902
+ const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
903
+
904
+ for (const [sliceId, component] of this.activeComponents) {
905
+ if (regex.test(sliceId)) {
906
+ componentsToDestroy.push(component);
907
+ }
908
+ }
909
+
910
+ if (componentsToDestroy.length === 0) {
911
+ return 0;
912
+ }
913
+
914
+ const count = this.destroyComponent(componentsToDestroy);
915
+
916
+ if (count > 0) {
917
+ slice.logger.logInfo(
918
+ 'Controller',
919
+ `Destroyed ${count} component(s) matching pattern: ${pattern} (including nested)`
920
+ );
921
+ }
922
+
923
+ return count;
924
+ }
925
+ }