slicejs-web-framework 3.4.4 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/Slice/Components/Structural/Controller/Controller.js +135 -0
  2. package/Slice/Components/Structural/EventManager/EventManager.js +289 -4
  3. package/Slice/Components/Structural/EventManager/EventManagerDebugger.js +368 -30
  4. package/Slice/Components/Structural/LeakInspector/LeakInspector.js +296 -0
  5. package/Slice/Slice.js +14 -1
  6. package/Slice/tests/event-emitters.test.js +79 -0
  7. package/Slice/tests/event-registry.test.js +141 -0
  8. package/Slice/tests/leak-orphans.test.js +131 -0
  9. package/api/index.js +35 -8
  10. package/package.json +1 -1
  11. package/sliceConfig.schema.json +10 -1
  12. package/src/sliceConfig.json +5 -1
  13. package/types/index.d.ts +12 -0
  14. package/slicejs-cli/.github/workflows/ci.yml +0 -43
  15. package/slicejs-cli/LICENSE +0 -21
  16. package/slicejs-cli/assets/Slice.js-logo.png +0 -0
  17. package/slicejs-cli/client.js +0 -740
  18. package/slicejs-cli/commands/Print.js +0 -164
  19. package/slicejs-cli/commands/Validations.js +0 -92
  20. package/slicejs-cli/commands/build/build.js +0 -40
  21. package/slicejs-cli/commands/buildProduction/buildProduction.js +0 -577
  22. package/slicejs-cli/commands/bundle/bundle.js +0 -236
  23. package/slicejs-cli/commands/createComponent/VisualComponentTemplate.js +0 -55
  24. package/slicejs-cli/commands/createComponent/createComponent.js +0 -128
  25. package/slicejs-cli/commands/deleteComponent/deleteComponent.js +0 -81
  26. package/slicejs-cli/commands/doctor/doctor.js +0 -516
  27. package/slicejs-cli/commands/getComponent/getComponent.js +0 -695
  28. package/slicejs-cli/commands/init/init.js +0 -473
  29. package/slicejs-cli/commands/listComponents/listComponents.js +0 -172
  30. package/slicejs-cli/commands/startServer/startServer.js +0 -240
  31. package/slicejs-cli/commands/startServer/watchServer.js +0 -66
  32. package/slicejs-cli/commands/types/types.js +0 -583
  33. package/slicejs-cli/commands/utils/LocalCliDelegation.js +0 -53
  34. package/slicejs-cli/commands/utils/PackageManager.js +0 -148
  35. package/slicejs-cli/commands/utils/PathHelper.js +0 -75
  36. package/slicejs-cli/commands/utils/VersionChecker.js +0 -169
  37. package/slicejs-cli/commands/utils/bundling/BundleGenerator.js +0 -2525
  38. package/slicejs-cli/commands/utils/bundling/DependencyAnalyzer.js +0 -933
  39. package/slicejs-cli/commands/utils/loadConfig.js +0 -31
  40. package/slicejs-cli/commands/utils/sliceScripts.js +0 -48
  41. package/slicejs-cli/commands/utils/updateManager.js +0 -471
  42. package/slicejs-cli/package.json +0 -71
  43. package/slicejs-cli/playwright.config.js +0 -51
  44. package/slicejs-cli/pnpm-lock.yaml +0 -1629
  45. package/slicejs-cli/tests/build-command-integration.test.js +0 -87
  46. package/slicejs-cli/tests/build-mangling-e2e.test.js +0 -290
  47. package/slicejs-cli/tests/build-production-e2e.test.js +0 -140
  48. package/slicejs-cli/tests/builder-edge-cases.test.js +0 -322
  49. package/slicejs-cli/tests/bundle-generate-e2e.test.js +0 -115
  50. package/slicejs-cli/tests/bundle-generator.test.js +0 -691
  51. package/slicejs-cli/tests/bundle-v2-register-output.test.js +0 -470
  52. package/slicejs-cli/tests/bundling-dependency-edges.test.js +0 -127
  53. package/slicejs-cli/tests/bundling-imports-unit.test.js +0 -267
  54. package/slicejs-cli/tests/client-launcher-contract.test.js +0 -211
  55. package/slicejs-cli/tests/commands-component-crud.test.js +0 -128
  56. package/slicejs-cli/tests/commands-doctor.test.js +0 -152
  57. package/slicejs-cli/tests/commands-version-checker.test.js +0 -37
  58. package/slicejs-cli/tests/component-registry-parse.test.js +0 -34
  59. package/slicejs-cli/tests/dependency-analyzer.test.js +0 -24
  60. package/slicejs-cli/tests/e2e/bundles.spec.js +0 -91
  61. package/slicejs-cli/tests/e2e/dependency-scenarios.spec.js +0 -56
  62. package/slicejs-cli/tests/e2e/fixtures/components/Service/FetchManager/FetchManager.js +0 -136
  63. package/slicejs-cli/tests/e2e/fixtures/components/Service/IndexedDbManager/IndexedDbManager.js +0 -149
  64. package/slicejs-cli/tests/e2e/fixtures/components/Service/LocalStorageManager/LocalStorageManager.js +0 -45
  65. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.css +0 -106
  66. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.html +0 -5
  67. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.js +0 -158
  68. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Link/Link.js +0 -33
  69. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.css +0 -56
  70. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.html +0 -83
  71. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.js +0 -164
  72. package/slicejs-cli/tests/e2e/fixtures/components/Visual/MultiRoute/MultiRoute.js +0 -167
  73. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.css +0 -116
  74. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.html +0 -44
  75. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.js +0 -180
  76. package/slicejs-cli/tests/e2e/fixtures/components/Visual/NotFound/NotFound.js +0 -20
  77. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Route/Route.js +0 -181
  78. package/slicejs-cli/tests/e2e/fixtures/components/registry.json +0 -12
  79. package/slicejs-cli/tests/e2e/fixtures/vendor-components.mjs +0 -65
  80. package/slicejs-cli/tests/e2e/navigation.spec.js +0 -44
  81. package/slicejs-cli/tests/e2e/render.spec.js +0 -34
  82. package/slicejs-cli/tests/e2e/serve.mjs +0 -264
  83. package/slicejs-cli/tests/e2e/shared-deps.spec.js +0 -61
  84. package/slicejs-cli/tests/e2e/unminified.spec.js +0 -33
  85. package/slicejs-cli/tests/e2e-serve.test.js +0 -148
  86. package/slicejs-cli/tests/fixtures/components.js +0 -8
  87. package/slicejs-cli/tests/fixtures/sliceConfig.json +0 -74
  88. package/slicejs-cli/tests/getcomponent.test.js +0 -435
  89. package/slicejs-cli/tests/helpers/setup.js +0 -102
  90. package/slicejs-cli/tests/init-command-contract.test.js +0 -101
  91. package/slicejs-cli/tests/init-e2e.test.js +0 -82
  92. package/slicejs-cli/tests/init-project-isolation.test.js +0 -98
  93. package/slicejs-cli/tests/local-cli-delegation.test.js +0 -81
  94. package/slicejs-cli/tests/package-manager.test.js +0 -127
  95. package/slicejs-cli/tests/path-helper.test.js +0 -206
  96. package/slicejs-cli/tests/perf-budget.test.js +0 -84
  97. package/slicejs-cli/tests/postinstall-command.test.js +0 -103
  98. package/slicejs-cli/tests/property-mangling-regression.test.js +0 -182
  99. package/slicejs-cli/tests/types-breakage.test.js +0 -491
  100. package/slicejs-cli/tests/types-generator-errors.test.js +0 -361
  101. package/slicejs-cli/tests/types-generator.test.js +0 -348
@@ -26,6 +26,12 @@ export default class Controller {
26
26
  this._pendingBuilds = new Map();
27
27
 
28
28
  this.idCounter = 0;
29
+
30
+ // 🔍 Leak detection (dev-only). App-provided predicates that mark a
31
+ // detached-but-registered component as intentional (not a leak).
32
+ this._leakExclusions = [];
33
+ // Ring buffer of activeComponents.size samples for the growth heuristic.
34
+ this._growthSamples = [];
29
35
  }
30
36
 
31
37
  /**
@@ -1268,4 +1274,133 @@ export default class Controller {
1268
1274
 
1269
1275
  return count;
1270
1276
  }
1277
+
1278
+ // ============================================================
1279
+ // 🔍 LEAK DETECTION (dev-only)
1280
+ // A component is a candidate leak when it stays registered in
1281
+ // activeComponents while detached from the live DOM and was not
1282
+ // intentionally cached (Route/MultiRoute/Router). Read-only.
1283
+ // ============================================================
1284
+
1285
+ /**
1286
+ * Register a predicate that excludes a detached component from being flagged
1287
+ * as a leak (e.g. caches that intentionally keep instances off-DOM).
1288
+ * @param {(component: HTMLElement) => boolean} predicate
1289
+ */
1290
+ registerLeakExclusion(predicate) {
1291
+ if (typeof predicate === 'function') {
1292
+ this._leakExclusions.push(predicate);
1293
+ }
1294
+ }
1295
+
1296
+ /**
1297
+ * Is this detached component intentionally retained (not a leak)?
1298
+ * @param {HTMLElement} component
1299
+ * @returns {boolean}
1300
+ * @private
1301
+ */
1302
+ _isExcludedOrphan(component) {
1303
+ // (1) Cache-owner marker — Route/MultiRoute flag their cached instances.
1304
+ if (component.__sliceCached === true) return true;
1305
+ // (2) Router-managed instances are reused across navigation by sliceId.
1306
+ if (typeof component.sliceId === 'string' && /^route-/.test(component.sliceId)) return true;
1307
+ // (3) App-provided predicates.
1308
+ for (const predicate of this._leakExclusions) {
1309
+ try {
1310
+ if (predicate(component)) return true;
1311
+ } catch (error) {
1312
+ slice.logger.logError('Controller', 'Leak exclusion predicate threw', error);
1313
+ }
1314
+ }
1315
+ return false;
1316
+ }
1317
+
1318
+ /**
1319
+ * Does an ancestor explain this component's detachment? If so we report only
1320
+ * the detached root, not every descendant of a removed/cached subtree.
1321
+ * @param {HTMLElement} component
1322
+ * @param {Set<string>} detachedIds
1323
+ * @returns {boolean}
1324
+ * @private
1325
+ */
1326
+ _hasDetachedAncestor(component, detachedIds) {
1327
+ let parent = component.parentComponent;
1328
+ while (parent) {
1329
+ if ((parent.sliceId && detachedIds.has(parent.sliceId)) || this._isExcludedOrphan(parent)) {
1330
+ return true;
1331
+ }
1332
+ parent = parent.parentComponent;
1333
+ }
1334
+ return false;
1335
+ }
1336
+
1337
+ /**
1338
+ * Build a human-readable retain path (component → parent → …) for diagnosis.
1339
+ * @param {HTMLElement} component
1340
+ * @returns {string[]}
1341
+ * @private
1342
+ */
1343
+ _retainPath(component) {
1344
+ const path = [];
1345
+ let node = component;
1346
+ let guard = 0;
1347
+ while (node && guard++ < 50) {
1348
+ path.push(`${node.constructor?.name || 'Unknown'}(${node.sliceId || '?'})`);
1349
+ node = node.parentComponent;
1350
+ }
1351
+ return path;
1352
+ }
1353
+
1354
+ /**
1355
+ * Find candidate leaks: components registered in activeComponents but detached
1356
+ * from the live DOM and not intentionally cached. Pure read-only.
1357
+ * @returns {Array<{ sliceId: string, name: string, reason: string, retainPath: string[] }>}
1358
+ */
1359
+ findOrphans() {
1360
+ const detached = [];
1361
+ for (const [sliceId, component] of this.activeComponents) {
1362
+ if (component instanceof HTMLElement && !component.isConnected) {
1363
+ detached.push([sliceId, component]);
1364
+ }
1365
+ }
1366
+
1367
+ const detachedIds = new Set(detached.map(([sliceId]) => sliceId));
1368
+ const orphans = [];
1369
+
1370
+ for (const [sliceId, component] of detached) {
1371
+ if (this._isExcludedOrphan(component)) continue;
1372
+ if (this._hasDetachedAncestor(component, detachedIds)) continue;
1373
+ orphans.push({
1374
+ sliceId,
1375
+ name: component.constructor?.name || 'Unknown',
1376
+ reason: 'registered but detached from DOM without destroy',
1377
+ retainPath: this._retainPath(component)
1378
+ });
1379
+ }
1380
+
1381
+ return orphans;
1382
+ }
1383
+
1384
+ /**
1385
+ * Record an activeComponents.size sample for the monotonic-growth heuristic.
1386
+ * Called by the Leak Inspector on each refresh while it is open.
1387
+ */
1388
+ sampleComponentGrowth() {
1389
+ this._growthSamples.push(this.activeComponents.size);
1390
+ if (this._growthSamples.length > 20) this._growthSamples.shift();
1391
+ }
1392
+
1393
+ /**
1394
+ * Heuristic: has the active component count grown without ever shrinking
1395
+ * across the sampled window? Suggests an accumulation leak.
1396
+ * @returns {boolean}
1397
+ */
1398
+ isGrowthMonotonic() {
1399
+ const samples = this._growthSamples;
1400
+ if (samples.length < 5) return false;
1401
+ for (let i = 1; i < samples.length; i++) {
1402
+ if (samples[i] < samples[i - 1]) return false;
1403
+ }
1404
+ return samples[samples.length - 1] > samples[0];
1405
+ }
1271
1406
  }
@@ -29,17 +29,224 @@ export default class EventManager {
29
29
  // Contador de emits por evento en la sesión actual de grabación
30
30
  this.emitCounts = new Map();
31
31
 
32
+ // Atribución de emisores por evento (tracing): Map<eventName, Map<key, { sliceId?, name?, source?, count, lastTs }>>
33
+ // Se llena solo mientras un debugger está abierto (igual que emitHistory).
34
+ this.emitters = new Map();
35
+
32
36
  // Flag: solo grabamos cuando algún debugger UI está visible
33
37
  this._recording = false;
34
38
 
35
39
  // Contador para IDs únicos
36
40
  this.idCounter = 0;
41
+
42
+ // ── Event registry (catálogo declarado vía register()) ──
43
+ // Map<eventName, { description, payload }>
44
+ this.registry = new Map();
45
+ // Validación activa: se enciende al primer register(). En loose mode (sin
46
+ // registry) NO se advierte nada — comportamiento idéntico al histórico.
47
+ this._validating = false;
48
+ // Warn-once: nombres ya advertidos como no declarados (evita spam).
49
+ this.undeclaredSeen = new Set();
50
+ // Drift para el debugger: eventos usados (emit/subscribe) pero no declarados.
51
+ this.undeclared = new Set();
52
+ // Warn-once: nombres ya advertidos por no tener namespace.
53
+ this._noNamespaceSeen = new Set();
54
+
55
+ // Grafo estático (documentación) generado por `slice types generate`:
56
+ // { events: { name: { payload, emitters[], listeners[] } }, dynamic: {...} }.
57
+ // Complementa el tracing observacional (this.emitters / this.emitHistory).
58
+ this.graph = null;
37
59
  }
38
60
 
39
61
  init() {
40
62
  return true;
41
63
  }
42
64
 
65
+ // ============================================
66
+ // REGISTRY - Catálogo de eventos declarados
67
+ // ============================================
68
+
69
+ /**
70
+ * Declarar un catálogo de eventos. Mergeable: se puede llamar varias veces
71
+ * desde varios módulos. Enciende la validación dev (warn-on-undeclared).
72
+ *
73
+ * Dos formas:
74
+ * - register(catalog) con claves planas `'namespace:event'`.
75
+ * - register(namespace, catalog) que prefija cada clave con `namespace:`,
76
+ * para declarar un grupo sin repetir el prefijo.
77
+ *
78
+ * @param {string|Object<string, { description?: string, payload?: object|null }>} namespaceOrCatalog
79
+ * @param {Object<string, { description?: string, payload?: object|null }>} [maybeCatalog]
80
+ * @returns {EventManager} this (encadenable)
81
+ *
82
+ * @example
83
+ * slice.events.register({
84
+ * 'user:login': { description: 'User logged in', payload: { id: 'number', name: 'string' } },
85
+ * 'cart:cleared': { description: 'Cart emptied', payload: null },
86
+ * });
87
+ *
88
+ * @example
89
+ * // Agrupado por namespace
90
+ * slice.events.register('user', {
91
+ * login: { payload: { id: 'number', name: 'string' } },
92
+ * logout: { payload: null },
93
+ * }); // => 'user:login', 'user:logout'
94
+ */
95
+ register(namespaceOrCatalog, maybeCatalog) {
96
+ let prefix = '';
97
+ let catalog = namespaceOrCatalog;
98
+
99
+ if (typeof namespaceOrCatalog === 'string') {
100
+ const ns = namespaceOrCatalog.trim();
101
+ prefix = ns.endsWith(':') ? ns : `${ns}:`;
102
+ catalog = maybeCatalog;
103
+ }
104
+
105
+ if (!catalog || typeof catalog !== 'object') {
106
+ slice.logger.logError('EventManager', 'register() requiere un objeto catálogo de eventos');
107
+ return this;
108
+ }
109
+
110
+ // Al primer register() sembramos los eventos internos del framework para
111
+ // que nunca disparen falsos warnings, y activamos la validación.
112
+ if (!this._validating) {
113
+ this._seedFrameworkEvents();
114
+ this._validating = true;
115
+ }
116
+
117
+ for (const [key, definition] of Object.entries(catalog)) {
118
+ const eventName = `${prefix}${key}`;
119
+ this._warnIfNoNamespace(eventName);
120
+
121
+ const entry = {
122
+ description: definition?.description || '',
123
+ payload: definition?.payload ?? null,
124
+ };
125
+
126
+ const existing = this.registry.get(eventName);
127
+ if (existing && JSON.stringify(existing.payload) !== JSON.stringify(entry.payload)) {
128
+ slice.logger.logWarning(
129
+ 'EventManager',
130
+ `Evento "${eventName}" redeclarado con un payload distinto; se usa la última definición`
131
+ );
132
+ }
133
+
134
+ this.registry.set(eventName, entry);
135
+ // Si estaba marcado como drift, ya no lo está.
136
+ this.undeclared.delete(eventName);
137
+ this.undeclaredSeen.delete(eventName);
138
+ }
139
+
140
+ return this;
141
+ }
142
+
143
+ /**
144
+ * Cargar el grafo estático de eventos (manifest generado por el CLI). Es la capa
145
+ * de DOCUMENTACIÓN (emisores/oyentes por análisis de código, sin ejecutar), que
146
+ * complementa el tracing observacional en runtime.
147
+ * @param {{ events: object, dynamic?: object }} graph
148
+ * @returns {EventManager} this
149
+ */
150
+ loadGraph(graph) {
151
+ if (graph && typeof graph === 'object' && graph.events) {
152
+ this.graph = graph;
153
+ }
154
+ return this;
155
+ }
156
+
157
+ /**
158
+ * Emisores conocidos de un evento por análisis estático (documentación).
159
+ * @param {string} eventName
160
+ * @returns {Array<{ file: string, line: number, component: string|null }>}
161
+ */
162
+ staticEmittersOf(eventName) {
163
+ return this.graph?.events?.[eventName]?.emitters || [];
164
+ }
165
+
166
+ /**
167
+ * Oyentes conocidos de un evento por análisis estático (documentación).
168
+ * @param {string} eventName
169
+ * @returns {Array<{ file: string, line: number, component: string|null }>}
170
+ */
171
+ staticListenersOf(eventName) {
172
+ return this.graph?.events?.[eventName]?.listeners || [];
173
+ }
174
+
175
+ /**
176
+ * Namespace de un evento: el segmento antes del primer ':' (o null si no tiene).
177
+ * @param {string} eventName
178
+ * @returns {string|null}
179
+ */
180
+ namespaceOf(eventName) {
181
+ const index = typeof eventName === 'string' ? eventName.indexOf(':') : -1;
182
+ return index > 0 ? eventName.slice(0, index) : null;
183
+ }
184
+
185
+ /**
186
+ * Sembrar los eventos internos del framework en el registry.
187
+ * @private
188
+ */
189
+ _seedFrameworkEvents() {
190
+ const framework = {
191
+ 'router:change': { description: 'Router navigated to a new route', payload: null },
192
+ 'context:__created': { description: 'A context was created', payload: { name: 'string', state: 'object' } },
193
+ };
194
+ for (const [name, def] of Object.entries(framework)) {
195
+ if (!this.registry.has(name)) this.registry.set(name, def);
196
+ }
197
+ }
198
+
199
+ /**
200
+ * ¿El evento está declarado en el registry?
201
+ * @param {string} eventName
202
+ * @returns {boolean}
203
+ */
204
+ isDeclared(eventName) {
205
+ if (this.registry.has(eventName)) return true;
206
+ // Familia dinámica context:<name> emitida por el ContextManager.
207
+ if (eventName.startsWith('context:')) return true;
208
+ return false;
209
+ }
210
+
211
+ /**
212
+ * Advertir (una sola vez, solo en dev) si el evento no está declarado.
213
+ * @private
214
+ */
215
+ _warnIfUndeclared(eventName) {
216
+ if (!this._validating) return;
217
+ if (this.isDeclared(eventName)) return;
218
+
219
+ this.undeclared.add(eventName);
220
+ if (this.undeclaredSeen.has(eventName)) return;
221
+ this.undeclaredSeen.add(eventName);
222
+
223
+ if (!slice.isProduction || !slice.isProduction()) {
224
+ slice.logger.logWarning(
225
+ 'EventManager',
226
+ `Undeclared event "${eventName}" — register it via slice.events.register({ '${eventName}': { payload: ... } })`
227
+ );
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Advertir (una sola vez, solo en dev) si el evento no tiene namespace.
233
+ * Solo activo cuando hay un registry en uso (this._validating).
234
+ * @private
235
+ */
236
+ _warnIfNoNamespace(eventName) {
237
+ if (!this._validating) return;
238
+ if (typeof eventName !== 'string' || eventName.includes(':')) return;
239
+ if (this._noNamespaceSeen.has(eventName)) return;
240
+ this._noNamespaceSeen.add(eventName);
241
+
242
+ if (!slice.isProduction || !slice.isProduction()) {
243
+ slice.logger.logWarning(
244
+ 'EventManager',
245
+ `Event "${eventName}" has no namespace — prefer "namespace:${eventName}" (e.g. group it with slice.events.register('app', { ${eventName}: ... })).`
246
+ );
247
+ }
248
+ }
249
+
43
250
  // ============================================
44
251
  // API PRINCIPAL
45
252
  // ============================================
@@ -68,6 +275,9 @@ export default class EventManager {
68
275
  return null;
69
276
  }
70
277
 
278
+ this._warnIfUndeclared(eventName);
279
+ this._warnIfNoNamespace(eventName);
280
+
71
281
  const subscriptionId = `evt_${++this.idCounter}`;
72
282
 
73
283
  // Crear Map para este evento si no existe
@@ -111,6 +321,7 @@ export default class EventManager {
111
321
  this._recording = true;
112
322
  this.emitHistory = [];
113
323
  this.emitCounts = new Map();
324
+ this.emitters = new Map();
114
325
  }
115
326
 
116
327
  /**
@@ -126,6 +337,9 @@ export default class EventManager {
126
337
  return null;
127
338
  }
128
339
 
340
+ this._warnIfUndeclared(eventName);
341
+ this._warnIfNoNamespace(eventName);
342
+
129
343
  const subscriptionId = `evt_${++this.idCounter}`;
130
344
 
131
345
  if (!this.subscriptions.has(eventName)) {
@@ -188,13 +402,31 @@ export default class EventManager {
188
402
  * slice.events.emit('cart:cleared'); // Sin datos
189
403
  */
190
404
  emit(eventName, ...args) {
405
+ // Global (unbound) emit — emitter resolved best-effort from the call stack.
406
+ this._emitFrom(null, eventName, args);
407
+ }
408
+
409
+ /**
410
+ * Core emit. `emitter` is the known source ({ sliceId, name }) when emitted via
411
+ * a bound API, or null for global emits (then a dev stack-trace fallback runs).
412
+ * @param {{ sliceId?: string, name?: string }|null} emitter
413
+ * @param {string} eventName
414
+ * @param {any[]} args
415
+ * @private
416
+ */
417
+ _emitFrom(emitter, eventName, args) {
191
418
  slice.logger.info('EventManager', `Emitting "${eventName}"`, args[0] ?? null);
192
419
 
193
- // Solo grabamos el histórico si algún debugger está abierto
420
+ this._warnIfUndeclared(eventName);
421
+ this._warnIfNoNamespace(eventName);
422
+
423
+ // Solo grabamos el histórico (y la atribución de emisores) si algún debugger está abierto.
194
424
  if (this._recording) {
195
- this.emitHistory.push({ eventName, timestamp: Date.now() });
425
+ const source = emitter || this._captureEmitterSource();
426
+ this.emitHistory.push({ eventName, timestamp: Date.now(), emitter: source });
196
427
  if (this.emitHistory.length > 500) this.emitHistory.shift();
197
428
  this.emitCounts.set(eventName, (this.emitCounts.get(eventName) || 0) + 1);
429
+ this._recordEmitter(eventName, source);
198
430
  }
199
431
 
200
432
  if (!this.subscriptions.has(eventName)) {
@@ -234,6 +466,47 @@ export default class EventManager {
234
466
  }
235
467
  }
236
468
 
469
+ /**
470
+ * Aggregate an emitter under an event: who emits, with a count. Powers the
471
+ * "emitted by" tracing view in the debugger.
472
+ * @param {{ sliceId?: string, name?: string, source?: string }|null} emitter
473
+ * @private
474
+ */
475
+ _recordEmitter(eventName, emitter) {
476
+ if (!emitter) return;
477
+ const key = emitter.sliceId || emitter.source || 'unknown';
478
+ if (!this.emitters.has(eventName)) this.emitters.set(eventName, new Map());
479
+ const bucket = this.emitters.get(eventName);
480
+ const existing = bucket.get(key);
481
+ if (existing) {
482
+ existing.count += 1;
483
+ existing.lastTs = Date.now();
484
+ } else {
485
+ bucket.set(key, { ...emitter, count: 1, lastTs: Date.now() });
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Best-effort source of a global emit from the call stack (dev only). Returns
491
+ * { source: 'file.js:line' } or null. Robust: never throws, never runs in prod.
492
+ * @private
493
+ */
494
+ _captureEmitterSource() {
495
+ if (slice.isProduction && slice.isProduction()) return null;
496
+ try {
497
+ const stack = new Error().stack || '';
498
+ for (const line of stack.split('\n')) {
499
+ // Skip the Error header and every frame inside this manager.
500
+ if (line.includes('EventManager.js')) continue;
501
+ const match = line.match(/([^/\\\s(]+\.js):(\d+):\d+/);
502
+ if (match) return { source: `${match[1]}:${match[2]}` };
503
+ }
504
+ } catch {
505
+ /* tracing is best-effort */
506
+ }
507
+ return null;
508
+ }
509
+
237
510
  // ============================================
238
511
  // BIND - Vinculación a Componente
239
512
  // ============================================
@@ -278,10 +551,22 @@ export default class EventManager {
278
551
  },
279
552
 
280
553
  /**
281
- * Emitir un evento
554
+ * Emitir un evento (atribuido a este componente para el tracing).
282
555
  */
283
556
  emit: (eventName, data) => {
284
- self.emit(eventName, data);
557
+ self._emitFrom(
558
+ { sliceId: component.sliceId, name: component.constructor?.name || 'Component' },
559
+ eventName,
560
+ [data]
561
+ );
562
+ },
563
+
564
+ /**
565
+ * Declarar eventos en el registry (passthrough a register()).
566
+ */
567
+ register: (namespaceOrCatalog, maybeCatalog) => {
568
+ self.register(namespaceOrCatalog, maybeCatalog);
569
+ return self;
285
570
  },
286
571
  };
287
572
  }