slicejs-web-framework 2.3.1 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,925 +1,944 @@
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
- }
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
+ /**
548
+ * Get a registered component by sliceId.
549
+ * @param {string} sliceId
550
+ * @returns {HTMLElement|undefined}
551
+ */
552
+ getComponent(sliceId) {
553
+ return this.activeComponents.get(sliceId);
554
+ }
555
+
556
+ loadTemplateToComponent(component) {
557
+ const className = component.constructor.name;
558
+ const template = this.templates.get(className);
559
+
560
+ if (!template) {
561
+ slice.logger.logError(`Template not found for component: ${className}`);
562
+ return;
563
+ }
564
+
565
+ component.innerHTML = template.innerHTML;
566
+ return component;
567
+ }
568
+
569
+ getComponentCategory(componentSliceId) {
570
+ return this.componentCategories.get(componentSliceId);
571
+ }
572
+
573
+ /**
574
+ * Fetch component resources (html, css, styles, theme).
575
+ * @param {string} componentName
576
+ * @param {'html'|'css'|'theme'|'styles'} resourceType
577
+ * @param {string} [componentCategory]
578
+ * @param {string} [customPath]
579
+ * @returns {Promise<string>}
580
+ */
581
+ async fetchText(componentName, resourceType, componentCategory, customPath) {
582
+ try {
583
+ const baseUrl = window.location.origin;
584
+ let path;
585
+
586
+ if (!componentCategory) {
587
+ componentCategory = this.componentCategories.get(componentName);
588
+ }
589
+
590
+ let isVisual = resourceType === 'html' || resourceType === 'css';
591
+
592
+ if (isVisual) {
593
+ if (slice.paths.components[componentCategory]) {
594
+ path = `${baseUrl}${slice.paths.components[componentCategory].path}/${componentName}`;
595
+ resourceType === 'html' ? (path += `/${componentName}.html`) : (path += `/${componentName}.css`);
596
+ } else {
597
+ if (componentCategory === 'Structural') {
598
+ path = `${baseUrl}/Slice/Components/Structural/${componentName}`;
599
+ resourceType === 'html' ? (path += `/${componentName}.html`) : (path += `/${componentName}.css`);
600
+ } else {
601
+ throw new Error(`Component category '${componentCategory}' not found in paths configuration`);
602
+ }
603
+ }
604
+ }
605
+
606
+ if (resourceType === 'theme') {
607
+ path = `${baseUrl}${slice.paths.themes}/${componentName}.css`;
608
+ }
609
+
610
+ if (resourceType === 'styles') {
611
+ path = `${baseUrl}${slice.paths.styles}/${componentName}.css`;
612
+ }
613
+
614
+ if (customPath) {
615
+ path = customPath;
616
+ }
617
+
618
+ slice.logger.logInfo('Controller', `Fetching ${resourceType} from: ${path}`);
619
+
620
+ const response = await fetch(path);
621
+
622
+ if (!response.ok) {
623
+ throw new Error(`Failed to fetch ${path}: ${response.statusText}`);
624
+ }
625
+
626
+ const content = await response.text();
627
+ slice.logger.logInfo('Controller', `Successfully fetched ${resourceType} for ${componentName}`);
628
+ return content;
629
+ } catch (error) {
630
+ slice.logger.logError('Controller', `Error fetching ${resourceType} for component ${componentName}:`, error);
631
+ throw error;
632
+ }
633
+ }
634
+
635
+ /**
636
+ * Apply props to a component using static defaults and setters.
637
+ * @param {HTMLElement} component
638
+ * @param {Object} props
639
+ * @returns {void}
640
+ */
641
+ setComponentProps(component, props) {
642
+ const ComponentClass = component.constructor;
643
+ const componentName = ComponentClass.name;
644
+
645
+ // Aplicar defaults si tiene static props
646
+ if (ComponentClass.props) {
647
+ this.applyDefaultProps(component, ComponentClass.props, props);
648
+ }
649
+
650
+ // Validar solo en desarrollo
651
+ if (ComponentClass.props && !slice.isProduction()) {
652
+ this.validatePropsInDevelopment(ComponentClass, props, componentName);
653
+ }
654
+
655
+ // Aplicar props
656
+ for (const prop in props) {
657
+ component[`_${prop}`] = null;
658
+ component[prop] = props[prop];
659
+ }
660
+ }
661
+
662
+ getComponentPropsForDebugger(component) {
663
+ const ComponentClass = component.constructor;
664
+
665
+ if (ComponentClass.props) {
666
+ return {
667
+ availableProps: Object.keys(ComponentClass.props),
668
+ propsConfig: ComponentClass.props,
669
+ usedProps: this.extractUsedProps(component, ComponentClass.props),
670
+ };
671
+ } else {
672
+ return {
673
+ availableProps: this.extractUsedProps(component),
674
+ propsConfig: null,
675
+ usedProps: this.extractUsedProps(component),
676
+ };
677
+ }
678
+ }
679
+
680
+ applyDefaultProps(component, staticProps, providedProps) {
681
+ Object.entries(staticProps).forEach(([prop, config]) => {
682
+ if (config.default !== undefined && !(prop in (providedProps || {}))) {
683
+ component[`_${prop}`] = null;
684
+ component[prop] = config.default;
685
+ }
686
+ });
687
+ }
688
+
689
+ validatePropsInDevelopment(ComponentClass, providedProps, componentName) {
690
+ const staticProps = ComponentClass.props;
691
+ const usedProps = Object.keys(providedProps || {});
692
+
693
+ const availableProps = Object.keys(staticProps);
694
+ const unknownProps = usedProps.filter((prop) => !availableProps.includes(prop));
695
+
696
+ if (unknownProps.length > 0) {
697
+ slice.logger.logWarning(
698
+ 'PropsValidator',
699
+ `${componentName}: Unknown props [${unknownProps.join(', ')}]. Available: [${availableProps.join(', ')}]`
700
+ );
701
+ }
702
+
703
+ const requiredProps = Object.entries(staticProps)
704
+ .filter(([_, config]) => config.required)
705
+ .map(([prop, _]) => prop);
706
+
707
+ const missingRequired = requiredProps.filter((prop) => !(prop in (providedProps || {})));
708
+ if (missingRequired.length > 0) {
709
+ slice.logger.logError(componentName, `Missing required props: [${missingRequired.join(', ')}]`);
710
+ }
711
+ }
712
+
713
+ extractUsedProps(component, staticProps = null) {
714
+ const usedProps = {};
715
+
716
+ if (staticProps) {
717
+ Object.keys(staticProps).forEach((prop) => {
718
+ if (component[prop] !== undefined) {
719
+ usedProps[prop] = component[prop];
720
+ }
721
+ });
722
+ } else {
723
+ Object.getOwnPropertyNames(component).forEach((key) => {
724
+ if (key.startsWith('_') && key !== '_isActive') {
725
+ const propName = key.substring(1);
726
+ usedProps[propName] = component[propName];
727
+ }
728
+ });
729
+ }
730
+
731
+ return usedProps;
732
+ }
733
+
734
+ // ============================================================================
735
+ // 🚀 MÉTODOS DE DESTRUCCIÓN OPTIMIZADOS
736
+ // ============================================================================
737
+
738
+ /**
739
+ * Encuentra recursivamente todos los hijos de un componente
740
+ * 🚀 OPTIMIZADO: O(m) en lugar de O(n*d) - usa childrenIndex
741
+ * @param {string} parentSliceId - sliceId del componente padre
742
+ * @param {Set<string>} collected - Set de sliceIds ya recolectados
743
+ * @returns {Set<string>} Set de todos los sliceIds de componentes hijos
744
+ */
745
+ findAllChildComponents(parentSliceId, collected = new Set()) {
746
+ // 🚀 Buscar directamente en el índice: O(1)
747
+ const children = this.childrenIndex.get(parentSliceId);
748
+
749
+ if (!children) return collected;
750
+
751
+ // 🚀 Iterar solo los hijos directos: O(k) donde k = número de hijos
752
+ for (const childSliceId of children) {
753
+ collected.add(childSliceId);
754
+ // Recursión solo sobre hijos, no todos los componentes
755
+ this.findAllChildComponents(childSliceId, collected);
756
+ }
757
+
758
+ return collected;
759
+ }
760
+
761
+ /**
762
+ * Encuentra recursivamente todos los componentes dentro de un contenedor DOM
763
+ * Útil para destroyByContainer cuando no tenemos el sliceId del padre
764
+ * @param {HTMLElement} container - Elemento contenedor
765
+ * @param {Set<string>} collected - Set de sliceIds ya recolectados
766
+ * @returns {Set<string>} Set de todos los sliceIds encontrados
767
+ */
768
+ findAllNestedComponentsInContainer(container, collected = new Set()) {
769
+ // Buscar todos los elementos con slice-id en el contenedor
770
+ const sliceComponents = container.querySelectorAll('[slice-id]');
771
+
772
+ sliceComponents.forEach((element) => {
773
+ const sliceId = element.getAttribute('slice-id') || element.sliceId;
774
+ if (sliceId && this.activeComponents.has(sliceId)) {
775
+ collected.add(sliceId);
776
+ // 🚀 Usar índice para buscar hijos recursivamente
777
+ this.findAllChildComponents(sliceId, collected);
778
+ }
779
+ });
780
+
781
+ return collected;
782
+ }
783
+
784
+ /**
785
+ * Destruye uno o múltiples componentes DE FORMA RECURSIVA
786
+ * 🚀 OPTIMIZADO: O(m log m) en lugar de O(n*d + m log m)
787
+ * @param {HTMLElement|Array<HTMLElement>|string|Array<string>} components
788
+ * @returns {number} Cantidad de componentes destruidos (incluyendo hijos)
789
+ */
790
+ destroyComponent(components) {
791
+ const toDestroy = Array.isArray(components) ? components : [components];
792
+ const allSliceIdsToDestroy = new Set();
793
+
794
+ // PASO 1: Recolectar todos los componentes padres y sus hijos recursivamente
795
+ for (const item of toDestroy) {
796
+ let sliceId = null;
797
+
798
+ if (typeof item === 'string') {
799
+ if (!this.activeComponents.has(item)) {
800
+ slice.logger.logWarning('Controller', `Component with sliceId "${item}" not found`);
801
+ continue;
802
+ }
803
+ sliceId = item;
804
+ } else if (item && item.sliceId) {
805
+ sliceId = item.sliceId;
806
+ } else {
807
+ slice.logger.logWarning('Controller', `Invalid component or sliceId provided to destroyComponent`);
808
+ continue;
809
+ }
810
+
811
+ allSliceIdsToDestroy.add(sliceId);
812
+
813
+ // 🚀 OPTIMIZADO: Usa childrenIndex en lugar de recorrer todos los componentes
814
+ this.findAllChildComponents(sliceId, allSliceIdsToDestroy);
815
+ }
816
+
817
+ // PASO 2: Ordenar por profundidad (más profundos primero)
818
+ // 🚀 OPTIMIZADO: Usa _depth precalculada en lugar de calcularla cada vez
819
+ const sortedSliceIds = Array.from(allSliceIdsToDestroy).sort((a, b) => {
820
+ const compA = this.activeComponents.get(a);
821
+ const compB = this.activeComponents.get(b);
822
+
823
+ if (!compA || !compB) return 0;
824
+
825
+ // 🚀 O(1) en lugar de O(d) - usa profundidad precalculada
826
+ return (compB._depth || 0) - (compA._depth || 0);
827
+ });
828
+
829
+ let destroyedCount = 0;
830
+
831
+ // PASO 3: Destruir en orden correcto (hijos antes que padres)
832
+ for (const sliceId of sortedSliceIds) {
833
+ const component = this.activeComponents.get(sliceId);
834
+
835
+ if (!component) continue;
836
+
837
+ // Ejecutar hook beforeDestroy si existe
838
+ if (typeof component.beforeDestroy === 'function') {
839
+ try {
840
+ component.beforeDestroy();
841
+ } catch (error) {
842
+ slice.logger.logError('Controller', `Error in beforeDestroy for ${sliceId}`, error);
843
+ }
844
+ }
845
+
846
+ // Limpiar suscripciones de eventos del componente
847
+ if (slice.events) {
848
+ slice.events.cleanupComponent(sliceId);
849
+ }
850
+
851
+ // 🚀 Limpiar del índice de hijos
852
+ this.childrenIndex.delete(sliceId);
853
+
854
+ // Si tiene padre, remover de la lista de hijos del padre
855
+ if (component.parentComponent) {
856
+ const parentChildren = this.childrenIndex.get(component.parentComponent.sliceId);
857
+ if (parentChildren) {
858
+ parentChildren.delete(sliceId);
859
+ // Si el padre no tiene más hijos, eliminar entrada vacía
860
+ if (parentChildren.size === 0) {
861
+ this.childrenIndex.delete(component.parentComponent.sliceId);
862
+ }
863
+ }
864
+ }
865
+
866
+ // Eliminar del mapa de componentes activos
867
+ this.activeComponents.delete(sliceId);
868
+
869
+ // Remover del DOM si está conectado
870
+ if (component.isConnected) {
871
+ component.remove();
872
+ }
873
+
874
+ destroyedCount++;
875
+ }
876
+
877
+ if (destroyedCount > 0) {
878
+ slice.logger.logInfo('Controller', `Destroyed ${destroyedCount} component(s) recursively`);
879
+ }
880
+
881
+ return destroyedCount;
882
+ }
883
+
884
+ /**
885
+ * Destruye todos los componentes Slice dentro de un contenedor (RECURSIVO)
886
+ * 🚀 OPTIMIZADO: Usa el índice inverso para búsqueda rápida
887
+ * @param {HTMLElement} container - Elemento contenedor
888
+ * @returns {number} Cantidad de componentes destruidos
889
+ */
890
+ destroyByContainer(container) {
891
+ if (!container) {
892
+ slice.logger.logWarning('Controller', 'No container provided to destroyByContainer');
893
+ return 0;
894
+ }
895
+
896
+ // 🚀 Recolectar componentes usando índice optimizado
897
+ const allSliceIds = this.findAllNestedComponentsInContainer(container);
898
+
899
+ if (allSliceIds.size === 0) {
900
+ return 0;
901
+ }
902
+
903
+ // Destruir usando el método principal optimizado
904
+ const count = this.destroyComponent(Array.from(allSliceIds));
905
+
906
+ if (count > 0) {
907
+ slice.logger.logInfo('Controller', `Destroyed ${count} component(s) from container (including nested)`);
908
+ }
909
+
910
+ return count;
911
+ }
912
+
913
+ /**
914
+ * Destruye componentes cuyos sliceId coincidan con un patrón (RECURSIVO)
915
+ * 🚀 OPTIMIZADO: Usa destrucción optimizada
916
+ * @param {string|RegExp} pattern - Patrón a buscar
917
+ * @returns {number} Cantidad de componentes destruidos
918
+ */
919
+ destroyByPattern(pattern) {
920
+ const componentsToDestroy = [];
921
+ const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
922
+
923
+ for (const [sliceId, component] of this.activeComponents) {
924
+ if (regex.test(sliceId)) {
925
+ componentsToDestroy.push(component);
926
+ }
927
+ }
928
+
929
+ if (componentsToDestroy.length === 0) {
930
+ return 0;
931
+ }
932
+
933
+ const count = this.destroyComponent(componentsToDestroy);
934
+
935
+ if (count > 0) {
936
+ slice.logger.logInfo(
937
+ 'Controller',
938
+ `Destroyed ${count} component(s) matching pattern: ${pattern} (including nested)`
939
+ );
940
+ }
941
+
942
+ return count;
943
+ }
944
+ }