slicejs-web-framework 3.4.2 → 3.4.4

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