slicejs-web-framework 3.2.3 → 3.3.1

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