slicejs-web-framework 3.2.1 → 3.2.3

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