eleva 1.0.0-alpha → 1.0.0-rc.10

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 (68) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +554 -137
  3. package/dist/eleva-plugins.cjs.js +3397 -0
  4. package/dist/eleva-plugins.cjs.js.map +1 -0
  5. package/dist/eleva-plugins.esm.js +3392 -0
  6. package/dist/eleva-plugins.esm.js.map +1 -0
  7. package/dist/eleva-plugins.umd.js +3403 -0
  8. package/dist/eleva-plugins.umd.js.map +1 -0
  9. package/dist/eleva-plugins.umd.min.js +3 -0
  10. package/dist/eleva-plugins.umd.min.js.map +1 -0
  11. package/dist/eleva.cjs.js +1448 -0
  12. package/dist/eleva.cjs.js.map +1 -0
  13. package/dist/eleva.d.ts +1057 -80
  14. package/dist/eleva.esm.js +1230 -274
  15. package/dist/eleva.esm.js.map +1 -1
  16. package/dist/eleva.umd.js +1230 -274
  17. package/dist/eleva.umd.js.map +1 -1
  18. package/dist/eleva.umd.min.js +3 -0
  19. package/dist/eleva.umd.min.js.map +1 -0
  20. package/dist/plugins/attr.umd.js +231 -0
  21. package/dist/plugins/attr.umd.js.map +1 -0
  22. package/dist/plugins/attr.umd.min.js +3 -0
  23. package/dist/plugins/attr.umd.min.js.map +1 -0
  24. package/dist/plugins/props.umd.js +711 -0
  25. package/dist/plugins/props.umd.js.map +1 -0
  26. package/dist/plugins/props.umd.min.js +3 -0
  27. package/dist/plugins/props.umd.min.js.map +1 -0
  28. package/dist/plugins/router.umd.js +1807 -0
  29. package/dist/plugins/router.umd.js.map +1 -0
  30. package/dist/plugins/router.umd.min.js +3 -0
  31. package/dist/plugins/router.umd.min.js.map +1 -0
  32. package/dist/plugins/store.umd.js +684 -0
  33. package/dist/plugins/store.umd.js.map +1 -0
  34. package/dist/plugins/store.umd.min.js +3 -0
  35. package/dist/plugins/store.umd.min.js.map +1 -0
  36. package/package.json +240 -62
  37. package/src/core/Eleva.js +552 -145
  38. package/src/modules/Emitter.js +154 -18
  39. package/src/modules/Renderer.js +288 -86
  40. package/src/modules/Signal.js +132 -13
  41. package/src/modules/TemplateEngine.js +153 -27
  42. package/src/plugins/Attr.js +252 -0
  43. package/src/plugins/Props.js +590 -0
  44. package/src/plugins/Router.js +1919 -0
  45. package/src/plugins/Store.js +741 -0
  46. package/src/plugins/index.js +40 -0
  47. package/types/core/Eleva.d.ts +482 -48
  48. package/types/core/Eleva.d.ts.map +1 -1
  49. package/types/modules/Emitter.d.ts +151 -20
  50. package/types/modules/Emitter.d.ts.map +1 -1
  51. package/types/modules/Renderer.d.ts +151 -12
  52. package/types/modules/Renderer.d.ts.map +1 -1
  53. package/types/modules/Signal.d.ts +130 -16
  54. package/types/modules/Signal.d.ts.map +1 -1
  55. package/types/modules/TemplateEngine.d.ts +154 -14
  56. package/types/modules/TemplateEngine.d.ts.map +1 -1
  57. package/types/plugins/Attr.d.ts +28 -0
  58. package/types/plugins/Attr.d.ts.map +1 -0
  59. package/types/plugins/Props.d.ts +48 -0
  60. package/types/plugins/Props.d.ts.map +1 -0
  61. package/types/plugins/Router.d.ts +1000 -0
  62. package/types/plugins/Router.d.ts.map +1 -0
  63. package/types/plugins/Store.d.ts +86 -0
  64. package/types/plugins/Store.d.ts.map +1 -0
  65. package/types/plugins/index.d.ts +5 -0
  66. package/types/plugins/index.d.ts.map +1 -0
  67. package/dist/eleva.min.js +0 -2
  68. package/dist/eleva.min.js.map +0 -1
package/dist/eleva.esm.js CHANGED
@@ -1,70 +1,276 @@
1
+ /*! Eleva v1.0.0-rc.10 | MIT License | https://elevajs.com */
2
+ // ============================================================================
3
+ // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
4
+ // ============================================================================
5
+
6
+ /**
7
+ * @typedef {Record<string, unknown>} TemplateData
8
+ * Data context for template interpolation
9
+ */
10
+
11
+ /**
12
+ * @typedef {string} TemplateString
13
+ * A string containing {{ expression }} interpolation markers
14
+ */
15
+
16
+ /**
17
+ * @typedef {string} Expression
18
+ * A JavaScript expression to be evaluated in the data context
19
+ */
20
+
1
21
  /**
2
- * 🔒 TemplateEngine: Secure interpolation & dynamic attribute parsing.
22
+ * @typedef {unknown} EvaluationResult
23
+ * The result of evaluating an expression (string, number, boolean, object, etc.)
24
+ */
25
+
26
+ /**
27
+ * @class 🔒 TemplateEngine
28
+ * @classdesc A secure template engine that handles interpolation and dynamic attribute parsing.
29
+ * Provides a way to evaluate expressions in templates.
30
+ * All methods are static and can be called directly on the class.
31
+ *
32
+ * Template Syntax:
33
+ * - `{{ expression }}` - Interpolate any JavaScript expression
34
+ * - `{{ variable }}` - Access data properties directly
35
+ * - `{{ object.property }}` - Access nested properties
36
+ * - `{{ condition ? a : b }}` - Ternary expressions
37
+ * - `{{ func(arg) }}` - Call functions from data context
38
+ *
39
+ * @example
40
+ * // Basic interpolation
41
+ * const template = "Hello, {{name}}!";
42
+ * const data = { name: "World" };
43
+ * const result = TemplateEngine.parse(template, data);
44
+ * // Result: "Hello, World!"
3
45
  *
4
- * This class provides methods to parse template strings by replacing
5
- * interpolation expressions with dynamic data values and to evaluate expressions
6
- * within a given data context.
46
+ * @example
47
+ * // Nested properties
48
+ * const template = "Welcome, {{user.name}}!";
49
+ * const data = { user: { name: "John" } };
50
+ * const result = TemplateEngine.parse(template, data);
51
+ * // Result: "Welcome, John!"
52
+ *
53
+ * @example
54
+ * // Expressions
55
+ * const template = "Status: {{active ? 'Online' : 'Offline'}}";
56
+ * const data = { active: true };
57
+ * const result = TemplateEngine.parse(template, data);
58
+ * // Result: "Status: Online"
59
+ *
60
+ * @example
61
+ * // With Signal values
62
+ * const template = "Count: {{count.value}}";
63
+ * const data = { count: { value: 42 } };
64
+ * const result = TemplateEngine.parse(template, data);
65
+ * // Result: "Count: 42"
7
66
  */
8
67
  class TemplateEngine {
9
68
  /**
10
- * Parses a template string and replaces interpolation expressions with corresponding values.
69
+ * Regular expression for matching template expressions in the format {{ expression }}
70
+ * Matches: {{ anything }} with optional whitespace inside braces
71
+ *
72
+ * @static
73
+ * @private
74
+ * @type {RegExp}
75
+ */
76
+ static expressionPattern = /\{\{\s*(.*?)\s*\}\}/g;
77
+
78
+ /**
79
+ * Parses a template string, replacing expressions with their evaluated values.
80
+ * Expressions are evaluated in the provided data context.
81
+ *
82
+ * @public
83
+ * @static
84
+ * @param {TemplateString|unknown} template - The template string to parse.
85
+ * @param {TemplateData} data - The data context for evaluating expressions.
86
+ * @returns {string} The parsed template with expressions replaced by their values.
87
+ *
88
+ * @example
89
+ * // Simple variables
90
+ * TemplateEngine.parse("Hello, {{name}}!", { name: "World" });
91
+ * // Result: "Hello, World!"
92
+ *
93
+ * @example
94
+ * // Nested properties
95
+ * TemplateEngine.parse("{{user.name}} is {{user.age}} years old", {
96
+ * user: { name: "John", age: 30 }
97
+ * });
98
+ * // Result: "John is 30 years old"
11
99
  *
12
- * @param {string} template - The template string containing expressions in the format {{ expression }}.
13
- * @param {object} data - The data object to use for evaluating expressions.
14
- * @returns {string} The resulting string with evaluated values.
100
+ * @example
101
+ * // Multiple expressions
102
+ * TemplateEngine.parse("{{greeting}}, {{name}}! You have {{count}} messages.", {
103
+ * greeting: "Hello",
104
+ * name: "User",
105
+ * count: 5
106
+ * });
107
+ * // Result: "Hello, User! You have 5 messages."
108
+ *
109
+ * @example
110
+ * // With conditionals
111
+ * TemplateEngine.parse("Status: {{online ? 'Active' : 'Inactive'}}", {
112
+ * online: true
113
+ * });
114
+ * // Result: "Status: Active"
15
115
  */
16
116
  static parse(template, data) {
17
- return template.replace(/\{\{\s*(.*?)\s*\}\}/g, (_, expr) => {
18
- const value = this.evaluate(expr, data);
19
- return value === undefined ? "" : value;
20
- });
117
+ if (typeof template !== "string") return template;
118
+ return template.replace(this.expressionPattern, (_, expression) => this.evaluate(expression, data));
21
119
  }
22
120
 
23
121
  /**
24
- * Evaluates an expression using the provided data context.
122
+ * Evaluates an expression in the context of the provided data object.
123
+ *
124
+ * Note: This does not provide a true sandbox and evaluated expressions may access global scope.
125
+ * The use of the `with` statement is necessary for expression evaluation but has security implications.
126
+ * Only use with trusted templates. User input should never be directly interpolated.
127
+ *
128
+ * @public
129
+ * @static
130
+ * @param {Expression|unknown} expression - The expression to evaluate.
131
+ * @param {TemplateData} data - The data context for evaluation.
132
+ * @returns {EvaluationResult} The result of the evaluation, or an empty string if evaluation fails.
133
+ *
134
+ * @example
135
+ * // Property access
136
+ * TemplateEngine.evaluate("user.name", { user: { name: "John" } });
137
+ * // Result: "John"
25
138
  *
26
- * @param {string} expr - The JavaScript expression to evaluate.
27
- * @param {object} data - The data context for evaluating the expression.
28
- * @returns {*} The result of the evaluated expression, or an empty string if undefined or on error.
139
+ * @example
140
+ * // Numeric values
141
+ * TemplateEngine.evaluate("user.age", { user: { age: 30 } });
142
+ * // Result: 30
143
+ *
144
+ * @example
145
+ * // Expressions
146
+ * TemplateEngine.evaluate("items.length > 0", { items: [1, 2, 3] });
147
+ * // Result: true
148
+ *
149
+ * @example
150
+ * // Function calls
151
+ * TemplateEngine.evaluate("formatDate(date)", {
152
+ * date: new Date(),
153
+ * formatDate: (d) => d.toISOString()
154
+ * });
155
+ * // Result: "2024-01-01T00:00:00.000Z"
156
+ *
157
+ * @example
158
+ * // Failed evaluation returns empty string
159
+ * TemplateEngine.evaluate("nonexistent.property", {});
160
+ * // Result: ""
29
161
  */
30
- static evaluate(expr, data) {
162
+ static evaluate(expression, data) {
163
+ if (typeof expression !== "string") return expression;
31
164
  try {
32
- const keys = Object.keys(data);
33
- const values = keys.map(k => data[k]);
34
- const result = new Function(...keys, `return ${expr}`)(...values);
35
- return result === undefined ? "" : result;
36
- } catch (error) {
37
- console.error(`Template evaluation error:`, {
38
- expression: expr,
39
- data,
40
- error: error.message
41
- });
165
+ return new Function("data", `with(data) { return ${expression}; }`)(data);
166
+ } catch {
42
167
  return "";
43
168
  }
44
169
  }
45
170
  }
46
171
 
172
+ // ============================================================================
173
+ // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
174
+ // ============================================================================
175
+
47
176
  /**
48
- * Signal: Fine-grained reactivity.
177
+ * @template T
178
+ * @callback SignalWatcher
179
+ * @param {T} value - The new value of the signal
180
+ * @returns {void}
181
+ */
182
+
183
+ /**
184
+ * @callback SignalUnsubscribe
185
+ * @returns {boolean} True if the watcher was successfully removed
186
+ */
187
+
188
+ /**
189
+ * @template T
190
+ * @typedef {Object} SignalLike
191
+ * @property {T} value - The current value
192
+ * @property {function(SignalWatcher<T>): SignalUnsubscribe} watch - Subscribe to changes
193
+ */
194
+
195
+ /**
196
+ * @class ⚡ Signal
197
+ * @classdesc A reactive data holder that enables fine-grained reactivity in the Eleva framework.
198
+ * Signals notify registered watchers when their value changes, enabling efficient DOM updates
199
+ * through targeted patching rather than full re-renders.
200
+ * Updates are batched using microtasks to prevent multiple synchronous notifications.
201
+ * The class is generic, allowing type-safe handling of any value type T.
202
+ *
203
+ * @template T The type of value held by this signal
49
204
  *
50
- * A reactive data holder that notifies registered watchers when its value changes,
51
- * allowing for fine-grained DOM patching rather than full re-renders.
205
+ * @example
206
+ * // Basic usage
207
+ * const count = new Signal(0);
208
+ * count.watch((value) => console.log(`Count changed to: ${value}`));
209
+ * count.value = 1; // Logs: "Count changed to: 1"
210
+ *
211
+ * @example
212
+ * // With unsubscribe
213
+ * const name = new Signal("John");
214
+ * const unsubscribe = name.watch((value) => console.log(value));
215
+ * name.value = "Jane"; // Logs: "Jane"
216
+ * unsubscribe(); // Stop watching
217
+ * name.value = "Bob"; // No log output
218
+ *
219
+ * @example
220
+ * // With objects
221
+ * /** @type {Signal<{x: number, y: number}>} *\/
222
+ * const position = new Signal({ x: 0, y: 0 });
223
+ * position.value = { x: 10, y: 20 }; // Triggers watchers
224
+ *
225
+ * @implements {SignalLike<T>}
52
226
  */
53
227
  class Signal {
54
228
  /**
55
- * Creates a new Signal instance.
229
+ * Creates a new Signal instance with the specified initial value.
230
+ *
231
+ * @public
232
+ * @param {T} value - The initial value of the signal.
233
+ *
234
+ * @example
235
+ * // Primitive types
236
+ * const count = new Signal(0); // Signal<number>
237
+ * const name = new Signal("John"); // Signal<string>
238
+ * const active = new Signal(true); // Signal<boolean>
56
239
  *
57
- * @param {*} value - The initial value of the signal.
240
+ * @example
241
+ * // Complex types (use JSDoc for type inference)
242
+ * /** @type {Signal<string[]>} *\/
243
+ * const items = new Signal([]);
244
+ *
245
+ * /** @type {Signal<{id: number, name: string} | null>} *\/
246
+ * const user = new Signal(null);
58
247
  */
59
248
  constructor(value) {
249
+ /**
250
+ * Internal storage for the signal's current value
251
+ * @private
252
+ * @type {T}
253
+ */
60
254
  this._value = value;
255
+ /**
256
+ * Collection of callback functions to be notified when value changes
257
+ * @private
258
+ * @type {Set<SignalWatcher<T>>}
259
+ */
61
260
  this._watchers = new Set();
261
+ /**
262
+ * Flag to prevent multiple synchronous watcher notifications
263
+ * @private
264
+ * @type {boolean}
265
+ */
266
+ this._pending = false;
62
267
  }
63
268
 
64
269
  /**
65
270
  * Gets the current value of the signal.
66
271
  *
67
- * @returns {*} The current value.
272
+ * @public
273
+ * @returns {T} The current value.
68
274
  */
69
275
  get value() {
70
276
  return this._value;
@@ -72,417 +278,1167 @@ class Signal {
72
278
 
73
279
  /**
74
280
  * Sets a new value for the signal and notifies all registered watchers if the value has changed.
281
+ * The notification is batched using microtasks to prevent multiple synchronous updates.
75
282
  *
76
- * @param {*} newVal - The new value to set.
283
+ * @public
284
+ * @param {T} newVal - The new value to set.
285
+ * @returns {void}
77
286
  */
78
287
  set value(newVal) {
79
- if (newVal !== this._value) {
80
- this._value = newVal;
81
- this._watchers.forEach(fn => fn(newVal));
82
- }
288
+ if (this._value === newVal) return;
289
+ this._value = newVal;
290
+ this._notify();
83
291
  }
84
292
 
85
293
  /**
86
294
  * Registers a watcher function that will be called whenever the signal's value changes.
295
+ * The watcher will receive the new value as its argument.
296
+ *
297
+ * @public
298
+ * @param {SignalWatcher<T>} fn - The callback function to invoke on value change.
299
+ * @returns {SignalUnsubscribe} A function to unsubscribe the watcher.
300
+ *
301
+ * @example
302
+ * // Basic watching
303
+ * const unsubscribe = signal.watch((value) => console.log(value));
87
304
  *
88
- * @param {Function} fn - The callback function to invoke on value change.
89
- * @returns {Function} A function to unsubscribe the watcher.
305
+ * @example
306
+ * // Stop watching
307
+ * unsubscribe(); // Returns true if watcher was removed
308
+ *
309
+ * @example
310
+ * // Multiple watchers
311
+ * const unsub1 = signal.watch((v) => console.log("Watcher 1:", v));
312
+ * const unsub2 = signal.watch((v) => console.log("Watcher 2:", v));
313
+ * signal.value = "test"; // Both watchers are called
90
314
  */
91
315
  watch(fn) {
92
316
  this._watchers.add(fn);
93
317
  return () => this._watchers.delete(fn);
94
318
  }
319
+
320
+ /**
321
+ * Notifies all registered watchers of a value change using microtask scheduling.
322
+ * Uses a pending flag to batch multiple synchronous updates into a single notification.
323
+ * All watcher callbacks receive the current value when executed.
324
+ *
325
+ * @private
326
+ * @returns {void}
327
+ */
328
+ _notify() {
329
+ if (this._pending) return;
330
+ this._pending = true;
331
+ queueMicrotask(() => {
332
+ /** @type {(fn: (value: T) => void) => void} */
333
+ this._watchers.forEach(fn => fn(this._value));
334
+ this._pending = false;
335
+ });
336
+ }
95
337
  }
96
338
 
339
+ // ============================================================================
340
+ // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
341
+ // ============================================================================
342
+
343
+ /**
344
+ * @template T
345
+ * @callback EventHandler
346
+ * @param {...T} args - Event arguments
347
+ * @returns {void|Promise<void>}
348
+ */
349
+
350
+ /**
351
+ * @callback EventUnsubscribe
352
+ * @returns {void}
353
+ */
354
+
355
+ /**
356
+ * @typedef {`${string}:${string}`} EventName
357
+ * Event names follow the format 'namespace:action' (e.g., 'user:login', 'cart:update')
358
+ */
359
+
360
+ /**
361
+ * @typedef {Object} EmitterLike
362
+ * @property {function(string, EventHandler<unknown>): EventUnsubscribe} on - Subscribe to an event
363
+ * @property {function(string, EventHandler<unknown>=): void} off - Unsubscribe from an event
364
+ * @property {function(string, ...unknown): void} emit - Emit an event
365
+ */
366
+
97
367
  /**
98
- * 🎙️ Emitter: Robust inter-component communication with event bubbling.
368
+ * @class 📡 Emitter
369
+ * @classdesc A robust event emitter that enables inter-component communication through a publish-subscribe pattern.
370
+ * Components can emit events and listen for events from other components, facilitating loose coupling
371
+ * and reactive updates across the application.
372
+ * Events are handled synchronously in the order they were registered, with proper cleanup
373
+ * of unsubscribed handlers.
374
+ *
375
+ * Event names should follow the format 'namespace:action' for consistency and organization.
376
+ *
377
+ * @example
378
+ * // Basic usage
379
+ * const emitter = new Emitter();
380
+ * emitter.on('user:login', (user) => console.log(`User logged in: ${user.name}`));
381
+ * emitter.emit('user:login', { name: 'John' }); // Logs: "User logged in: John"
382
+ *
383
+ * @example
384
+ * // With unsubscribe
385
+ * const unsub = emitter.on('cart:update', (items) => {
386
+ * console.log(`Cart has ${items.length} items`);
387
+ * });
388
+ * emitter.emit('cart:update', [{ id: 1, name: 'Book' }]); // Logs: "Cart has 1 items"
389
+ * unsub(); // Stop listening
390
+ * emitter.emit('cart:update', []); // No log output
99
391
  *
100
- * Implements a basic publish-subscribe pattern for event handling,
101
- * allowing components to communicate through custom events.
392
+ * @example
393
+ * // Multiple arguments
394
+ * emitter.on('order:placed', (orderId, amount, currency) => {
395
+ * console.log(`Order ${orderId}: ${amount} ${currency}`);
396
+ * });
397
+ * emitter.emit('order:placed', 'ORD-123', 99.99, 'USD');
398
+ *
399
+ * @example
400
+ * // Common event patterns
401
+ * // Lifecycle events
402
+ * emitter.on('component:mount', (component) => {});
403
+ * emitter.on('component:unmount', (component) => {});
404
+ * // State events
405
+ * emitter.on('state:change', (newState, oldState) => {});
406
+ * // Navigation events
407
+ * emitter.on('router:navigate', (to, from) => {});
408
+ *
409
+ * @implements {EmitterLike}
102
410
  */
103
411
  class Emitter {
104
412
  /**
105
413
  * Creates a new Emitter instance.
414
+ *
415
+ * @public
416
+ *
417
+ * @example
418
+ * const emitter = new Emitter();
106
419
  */
107
420
  constructor() {
108
- /** @type {Object.<string, Function[]>} */
109
- this.events = {};
421
+ /**
422
+ * Map of event names to their registered handler functions
423
+ * @private
424
+ * @type {Map<string, Set<EventHandler<unknown>>>}
425
+ */
426
+ this._events = new Map();
110
427
  }
111
428
 
112
429
  /**
113
- * Registers an event handler for the specified event.
430
+ * Registers an event handler for the specified event name.
431
+ * The handler will be called with the event data when the event is emitted.
432
+ * Event names should follow the format 'namespace:action' for consistency.
433
+ *
434
+ * @public
435
+ * @template T
436
+ * @param {string} event - The name of the event to listen for (e.g., 'user:login').
437
+ * @param {EventHandler<T>} handler - The callback function to invoke when the event occurs.
438
+ * @returns {EventUnsubscribe} A function to unsubscribe the event handler.
439
+ *
440
+ * @example
441
+ * // Basic subscription
442
+ * const unsubscribe = emitter.on('user:login', (user) => console.log(user));
114
443
  *
115
- * @param {string} event - The name of the event.
116
- * @param {Function} handler - The function to call when the event is emitted.
444
+ * @example
445
+ * // Typed handler
446
+ * emitter.on('user:update', (/** @type {{id: number, name: string}} *\/ user) => {
447
+ * console.log(`User ${user.id}: ${user.name}`);
448
+ * });
449
+ *
450
+ * @example
451
+ * // Cleanup
452
+ * unsubscribe(); // Stops listening for the event
117
453
  */
118
454
  on(event, handler) {
119
- (this.events[event] || (this.events[event] = [])).push(handler);
455
+ if (!this._events.has(event)) this._events.set(event, new Set());
456
+ this._events.get(event).add(handler);
457
+ return () => this.off(event, handler);
120
458
  }
121
459
 
122
460
  /**
123
- * Removes a previously registered event handler.
461
+ * Removes an event handler for the specified event name.
462
+ * If no handler is provided, all handlers for the event are removed.
463
+ * Automatically cleans up empty event sets to prevent memory leaks.
464
+ *
465
+ * @public
466
+ * @template T
467
+ * @param {string} event - The name of the event to remove handlers from.
468
+ * @param {EventHandler<T>} [handler] - The specific handler function to remove.
469
+ * @returns {void}
124
470
  *
125
- * @param {string} event - The name of the event.
126
- * @param {Function} handler - The handler function to remove.
471
+ * @example
472
+ * // Remove a specific handler
473
+ * const loginHandler = (user) => console.log(user);
474
+ * emitter.on('user:login', loginHandler);
475
+ * emitter.off('user:login', loginHandler);
476
+ *
477
+ * @example
478
+ * // Remove all handlers for an event
479
+ * emitter.off('user:login');
127
480
  */
128
481
  off(event, handler) {
129
- if (this.events[event]) {
130
- this.events[event] = this.events[event].filter(h => h !== handler);
482
+ if (!this._events.has(event)) return;
483
+ if (handler) {
484
+ const handlers = this._events.get(event);
485
+ handlers.delete(handler);
486
+ // Remove the event if there are no handlers left
487
+ if (handlers.size === 0) this._events.delete(event);
488
+ } else {
489
+ this._events.delete(event);
131
490
  }
132
491
  }
133
492
 
134
493
  /**
135
- * Emits an event, invoking all handlers registered for that event.
494
+ * Emits an event with the specified data to all registered handlers.
495
+ * Handlers are called synchronously in the order they were registered.
496
+ * If no handlers are registered for the event, the emission is silently ignored.
497
+ *
498
+ * @public
499
+ * @template T
500
+ * @param {string} event - The name of the event to emit.
501
+ * @param {...T} args - Optional arguments to pass to the event handlers.
502
+ * @returns {void}
503
+ *
504
+ * @example
505
+ * // Emit an event with data
506
+ * emitter.emit('user:login', { name: 'John', role: 'admin' });
507
+ *
508
+ * @example
509
+ * // Emit an event with multiple arguments
510
+ * emitter.emit('order:placed', 'ORD-123', 99.99, 'USD');
136
511
  *
137
- * @param {string} event - The event name.
138
- * @param {...*} args - Additional arguments to pass to the event handlers.
512
+ * @example
513
+ * // Emit without data
514
+ * emitter.emit('app:ready');
139
515
  */
140
516
  emit(event, ...args) {
141
- (this.events[event] || []).forEach(handler => handler(...args));
517
+ if (!this._events.has(event)) return;
518
+ this._events.get(event).forEach(handler => handler(...args));
142
519
  }
143
520
  }
144
521
 
522
+ // ============================================================================
523
+ // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
524
+ // ============================================================================
525
+
526
+ /**
527
+ * @typedef {Object} PatchOptions
528
+ * @property {boolean} [preserveStyles=true]
529
+ * Whether to preserve style elements with data-e-style attribute
530
+ * @property {boolean} [preserveInstances=true]
531
+ * Whether to preserve elements with _eleva_instance property
532
+ */
533
+
534
+ /**
535
+ * @typedef {Map<string, Node>} KeyMap
536
+ * Map of key attribute values to their corresponding DOM nodes
537
+ */
538
+
539
+ /**
540
+ * @typedef {'ELEMENT_NODE'|'TEXT_NODE'|'COMMENT_NODE'|'DOCUMENT_FRAGMENT_NODE'} NodeTypeName
541
+ * Common DOM node type names
542
+ */
543
+
145
544
  /**
146
- * 🎨 Renderer: Handles DOM patching, diffing, and attribute updates.
545
+ * @class 🎨 Renderer
546
+ * @classdesc A high-performance DOM renderer that implements an optimized direct DOM diffing algorithm.
147
547
  *
148
- * Provides methods for efficient DOM updates by diffing the new and old DOM structures
149
- * and applying only the necessary changes.
548
+ * Key features:
549
+ * - Single-pass diffing algorithm for efficient DOM updates
550
+ * - Key-based node reconciliation for optimal performance
551
+ * - Intelligent attribute handling for ARIA, data attributes, and boolean properties
552
+ * - Preservation of special Eleva-managed instances and style elements
553
+ * - Memory-efficient with reusable temporary containers
554
+ *
555
+ * The renderer is designed to minimize DOM operations while maintaining
556
+ * exact attribute synchronization and proper node identity preservation.
557
+ * It's particularly optimized for frequent updates and complex DOM structures.
558
+ *
559
+ * @example
560
+ * // Basic usage
561
+ * const renderer = new Renderer();
562
+ * const container = document.getElementById("app");
563
+ * const newHtml = "<div>Updated content</div>";
564
+ * renderer.patchDOM(container, newHtml);
565
+ *
566
+ * @example
567
+ * // With keyed elements for optimal list updates
568
+ * const listHtml = `
569
+ * <ul>
570
+ * <li key="item-1">First</li>
571
+ * <li key="item-2">Second</li>
572
+ * <li key="item-3">Third</li>
573
+ * </ul>
574
+ * `;
575
+ * renderer.patchDOM(container, listHtml);
576
+ *
577
+ * @example
578
+ * // The renderer preserves Eleva-managed elements
579
+ * // Elements with _eleva_instance are not replaced during diffing
580
+ * // Style elements with data-e-style are preserved
150
581
  */
151
582
  class Renderer {
152
583
  /**
153
- * Patches the DOM of a container element with new HTML content.
584
+ * Creates a new Renderer instance.
585
+ *
586
+ * @public
154
587
  *
588
+ * @example
589
+ * const renderer = new Renderer();
590
+ */
591
+ constructor() {
592
+ /**
593
+ * A temporary container to hold the new HTML content while diffing.
594
+ * Reused across patch operations to minimize memory allocation.
595
+ * @private
596
+ * @type {HTMLDivElement}
597
+ */
598
+ this._tempContainer = document.createElement("div");
599
+ }
600
+
601
+ /**
602
+ * Patches the DOM of the given container with the provided HTML string.
603
+ * Uses an optimized diffing algorithm to minimize DOM operations.
604
+ *
605
+ * @public
155
606
  * @param {HTMLElement} container - The container element to patch.
156
- * @param {string} newHtml - The new HTML content to apply.
607
+ * @param {string} newHtml - The new HTML string.
608
+ * @returns {void}
609
+ * @throws {TypeError} If container is not an HTMLElement or newHtml is not a string.
610
+ * @throws {Error} If DOM patching fails.
611
+ *
612
+ * @example
613
+ * // Update container content
614
+ * renderer.patchDOM(container, '<div class="updated">New content</div>');
615
+ *
616
+ * @example
617
+ * // Update list with keys for optimal diffing
618
+ * const items = ['a', 'b', 'c'];
619
+ * const html = items.map(item =>
620
+ * `<li key="${item}">${item}</li>`
621
+ * ).join('');
622
+ * renderer.patchDOM(listContainer, `<ul>${html}</ul>`);
157
623
  */
158
624
  patchDOM(container, newHtml) {
159
- const tempContainer = document.createElement("div");
160
- tempContainer.innerHTML = newHtml;
161
- this.diff(container, tempContainer);
625
+ if (!(container instanceof HTMLElement)) {
626
+ throw new TypeError("Container must be an HTMLElement");
627
+ }
628
+ if (typeof newHtml !== "string") {
629
+ throw new TypeError("newHtml must be a string");
630
+ }
631
+ try {
632
+ this._tempContainer.innerHTML = newHtml;
633
+ this._diff(container, this._tempContainer);
634
+ } catch (error) {
635
+ throw new Error(`Failed to patch DOM: ${error.message}`);
636
+ }
162
637
  }
163
638
 
164
639
  /**
165
- * Diffs two DOM trees (old and new) and applies updates to the old DOM.
640
+ * Performs a diff between two DOM nodes and patches the old node to match the new node.
166
641
  *
642
+ * @private
167
643
  * @param {HTMLElement} oldParent - The original DOM element.
168
644
  * @param {HTMLElement} newParent - The new DOM element.
645
+ * @returns {void}
169
646
  */
170
- diff(oldParent, newParent) {
171
- const oldNodes = Array.from(oldParent.childNodes);
172
- const newNodes = Array.from(newParent.childNodes);
173
- const max = Math.max(oldNodes.length, newNodes.length);
174
- for (let i = 0; i < max; i++) {
175
- const oldNode = oldNodes[i];
176
- const newNode = newNodes[i];
177
-
178
- // Append new nodes that don't exist in the old tree.
179
- if (!oldNode && newNode) {
180
- oldParent.appendChild(newNode.cloneNode(true));
181
- continue;
182
- }
183
- // Remove old nodes not present in the new tree.
184
- if (oldNode && !newNode) {
185
- oldParent.removeChild(oldNode);
186
- continue;
187
- }
188
-
189
- // For element nodes, compare keys if available.
190
- if (oldNode.nodeType === Node.ELEMENT_NODE && newNode.nodeType === Node.ELEMENT_NODE) {
191
- const oldKey = oldNode.getAttribute("key");
192
- const newKey = newNode.getAttribute("key");
193
- if (oldKey || newKey) {
194
- if (oldKey !== newKey) {
195
- oldParent.replaceChild(newNode.cloneNode(true), oldNode);
196
- continue;
197
- }
647
+ _diff(oldParent, newParent) {
648
+ if (oldParent === newParent || oldParent.isEqualNode?.(newParent)) return;
649
+ const oldChildren = Array.from(oldParent.childNodes);
650
+ const newChildren = Array.from(newParent.childNodes);
651
+ let oldStartIdx = 0,
652
+ newStartIdx = 0;
653
+ let oldEndIdx = oldChildren.length - 1;
654
+ let newEndIdx = newChildren.length - 1;
655
+ let oldKeyMap = null;
656
+ while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
657
+ let oldStartNode = oldChildren[oldStartIdx];
658
+ let newStartNode = newChildren[newStartIdx];
659
+ if (!oldStartNode) {
660
+ oldStartNode = oldChildren[++oldStartIdx];
661
+ } else if (this._isSameNode(oldStartNode, newStartNode)) {
662
+ this._patchNode(oldStartNode, newStartNode);
663
+ oldStartIdx++;
664
+ newStartIdx++;
665
+ } else {
666
+ if (!oldKeyMap) {
667
+ oldKeyMap = this._createKeyMap(oldChildren, oldStartIdx, oldEndIdx);
198
668
  }
199
- }
200
-
201
- // Replace nodes if types or tag names differ.
202
- if (oldNode.nodeType !== newNode.nodeType || oldNode.nodeName !== newNode.nodeName) {
203
- oldParent.replaceChild(newNode.cloneNode(true), oldNode);
204
- continue;
205
- }
206
- // For text nodes, update content if different.
207
- if (oldNode.nodeType === Node.TEXT_NODE) {
208
- if (oldNode.nodeValue !== newNode.nodeValue) {
209
- oldNode.nodeValue = newNode.nodeValue;
669
+ const key = this._getNodeKey(newStartNode);
670
+ const oldNodeToMove = key ? oldKeyMap.get(key) : null;
671
+ if (oldNodeToMove) {
672
+ this._patchNode(oldNodeToMove, newStartNode);
673
+ oldParent.insertBefore(oldNodeToMove, oldStartNode);
674
+ oldChildren[oldChildren.indexOf(oldNodeToMove)] = null;
675
+ } else {
676
+ oldParent.insertBefore(newStartNode.cloneNode(true), oldStartNode);
210
677
  }
211
- continue;
678
+ newStartIdx++;
212
679
  }
213
- // For element nodes, update attributes and then diff children.
214
- if (oldNode.nodeType === Node.ELEMENT_NODE) {
215
- this.updateAttributes(oldNode, newNode);
216
- this.diff(oldNode, newNode);
680
+ }
681
+ if (oldStartIdx > oldEndIdx) {
682
+ const refNode = newChildren[newEndIdx + 1] ? oldChildren[oldStartIdx] : null;
683
+ for (let i = newStartIdx; i <= newEndIdx; i++) {
684
+ if (newChildren[i]) oldParent.insertBefore(newChildren[i].cloneNode(true), refNode);
685
+ }
686
+ } else if (newStartIdx > newEndIdx) {
687
+ for (let i = oldStartIdx; i <= oldEndIdx; i++) {
688
+ if (oldChildren[i]) this._removeNode(oldParent, oldChildren[i]);
217
689
  }
218
690
  }
219
691
  }
220
692
 
221
693
  /**
222
- * Updates the attributes of an element to match those of a new element.
694
+ * Patches a single node.
223
695
  *
224
- * @param {HTMLElement} oldEl - The element to update.
225
- * @param {HTMLElement} newEl - The element providing the updated attributes.
696
+ * @private
697
+ * @param {Node} oldNode - The original DOM node.
698
+ * @param {Node} newNode - The new DOM node.
699
+ * @returns {void}
226
700
  */
227
- updateAttributes(oldEl, newEl) {
228
- const attributeToPropertyMap = {
229
- value: "value",
230
- checked: "checked",
231
- selected: "selected",
232
- disabled: "disabled"
233
- };
701
+ _patchNode(oldNode, newNode) {
702
+ if (oldNode?._eleva_instance) return;
703
+ if (!this._isSameNode(oldNode, newNode)) {
704
+ oldNode.replaceWith(newNode.cloneNode(true));
705
+ return;
706
+ }
707
+ if (oldNode.nodeType === Node.ELEMENT_NODE) {
708
+ this._updateAttributes(oldNode, newNode);
709
+ this._diff(oldNode, newNode);
710
+ } else if (oldNode.nodeType === Node.TEXT_NODE && oldNode.nodeValue !== newNode.nodeValue) {
711
+ oldNode.nodeValue = newNode.nodeValue;
712
+ }
713
+ }
234
714
 
235
- // Remove old attributes that no longer exist.
236
- Array.from(oldEl.attributes).forEach(attr => {
237
- if (attr.name.startsWith("@")) return;
238
- if (!newEl.hasAttribute(attr.name)) {
239
- oldEl.removeAttribute(attr.name);
240
- }
241
- });
242
- // Add or update attributes from newEl.
243
- Array.from(newEl.attributes).forEach(attr => {
244
- if (attr.name.startsWith("@")) return;
245
- if (oldEl.getAttribute(attr.name) !== attr.value) {
246
- oldEl.setAttribute(attr.name, attr.value);
247
- if (attributeToPropertyMap[attr.name]) {
248
- oldEl[attributeToPropertyMap[attr.name]] = attr.value;
249
- } else if (attr.name in oldEl) {
250
- oldEl[attr.name] = attr.value;
251
- }
715
+ /**
716
+ * Removes a node from its parent.
717
+ *
718
+ * @private
719
+ * @param {HTMLElement} parent - The parent element containing the node to remove.
720
+ * @param {Node} node - The node to remove.
721
+ * @returns {void}
722
+ */
723
+ _removeNode(parent, node) {
724
+ if (node.nodeName === "STYLE" && node.hasAttribute("data-e-style")) return;
725
+ parent.removeChild(node);
726
+ }
727
+
728
+ /**
729
+ * Updates the attributes of an element to match a new element's attributes.
730
+ *
731
+ * @private
732
+ * @param {HTMLElement} oldEl - The original element to update.
733
+ * @param {HTMLElement} newEl - The new element to update.
734
+ * @returns {void}
735
+ */
736
+ _updateAttributes(oldEl, newEl) {
737
+ const oldAttrs = oldEl.attributes;
738
+ const newAttrs = newEl.attributes;
739
+
740
+ // Process new attributes
741
+ for (let i = 0; i < newAttrs.length; i++) {
742
+ const {
743
+ name,
744
+ value
745
+ } = newAttrs[i];
746
+
747
+ // Skip event attributes (handled by event system)
748
+ if (name.startsWith("@")) continue;
749
+
750
+ // Skip if attribute hasn't changed
751
+ if (oldEl.getAttribute(name) === value) continue;
752
+
753
+ // Basic attribute setting
754
+ oldEl.setAttribute(name, value);
755
+ }
756
+
757
+ // Remove old attributes that are no longer present
758
+ for (let i = oldAttrs.length - 1; i >= 0; i--) {
759
+ const name = oldAttrs[i].name;
760
+ if (!newEl.hasAttribute(name)) {
761
+ oldEl.removeAttribute(name);
252
762
  }
253
- });
763
+ }
764
+ }
765
+
766
+ /**
767
+ * Determines if two nodes are the same based on their type, name, and key attributes.
768
+ *
769
+ * @private
770
+ * @param {Node} oldNode - The first node to compare.
771
+ * @param {Node} newNode - The second node to compare.
772
+ * @returns {boolean} True if the nodes are considered the same, false otherwise.
773
+ */
774
+ _isSameNode(oldNode, newNode) {
775
+ if (!oldNode || !newNode) return false;
776
+ const oldKey = oldNode.nodeType === Node.ELEMENT_NODE ? oldNode.getAttribute("key") : null;
777
+ const newKey = newNode.nodeType === Node.ELEMENT_NODE ? newNode.getAttribute("key") : null;
778
+ if (oldKey && newKey) return oldKey === newKey;
779
+ return !oldKey && !newKey && oldNode.nodeType === newNode.nodeType && oldNode.nodeName === newNode.nodeName;
780
+ }
781
+
782
+ /**
783
+ * Creates a key map for the children of a parent node.
784
+ * Used for efficient O(1) lookup of keyed elements during diffing.
785
+ *
786
+ * @private
787
+ * @param {Array<ChildNode>} children - The children of the parent node.
788
+ * @param {number} start - The start index of the children.
789
+ * @param {number} end - The end index of the children.
790
+ * @returns {KeyMap} A key map for the children.
791
+ */
792
+ _createKeyMap(children, start, end) {
793
+ const map = new Map();
794
+ for (let i = start; i <= end; i++) {
795
+ const child = children[i];
796
+ const key = this._getNodeKey(child);
797
+ if (key) map.set(key, child);
798
+ }
799
+ return map;
800
+ }
801
+
802
+ /**
803
+ * Extracts the key attribute from a node if it exists.
804
+ *
805
+ * @private
806
+ * @param {Node} node - The node to extract the key from.
807
+ * @returns {string|null} The key attribute value or null if not found.
808
+ */
809
+ _getNodeKey(node) {
810
+ return node?.nodeType === Node.ELEMENT_NODE ? node.getAttribute("key") : null;
254
811
  }
255
812
  }
256
813
 
814
+ // ============================================================================
815
+ // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
816
+ // ============================================================================
817
+
818
+ // -----------------------------------------------------------------------------
819
+ // Configuration Types
820
+ // -----------------------------------------------------------------------------
821
+
822
+ /**
823
+ * @typedef {Object} ElevaConfig
824
+ * @property {boolean} [debug=false]
825
+ * Enable debug mode for verbose logging
826
+ * @property {string} [prefix='e']
827
+ * Prefix for component style scoping
828
+ * @property {boolean} [async=true]
829
+ * Enable async component setup
830
+ */
831
+
832
+ // -----------------------------------------------------------------------------
833
+ // Component Types
834
+ // -----------------------------------------------------------------------------
835
+
836
+ /**
837
+ * @typedef {Object} ComponentDefinition
838
+ * @property {SetupFunction} [setup]
839
+ * Optional setup function that initializes the component's state and returns reactive data
840
+ * @property {TemplateFunction|string} template
841
+ * Required function or string that defines the component's HTML structure
842
+ * @property {StyleFunction|string} [style]
843
+ * Optional function or string that provides component-scoped CSS styles
844
+ * @property {ChildrenMap} [children]
845
+ * Optional object defining nested child components
846
+ */
847
+
848
+ /**
849
+ * @callback SetupFunction
850
+ * @param {ComponentContext} ctx - The component context with props, emitter, and signal factory
851
+ * @returns {SetupResult|Promise<SetupResult>} Reactive data and lifecycle hooks
852
+ */
853
+
854
+ /**
855
+ * @typedef {Record<string, unknown> & LifecycleHooks} SetupResult
856
+ * Data returned from setup function, may include lifecycle hooks
857
+ */
858
+
859
+ /**
860
+ * @callback TemplateFunction
861
+ * @param {ComponentContext} ctx - The component context
862
+ * @returns {string|Promise<string>} HTML template string
863
+ */
864
+
865
+ /**
866
+ * @callback StyleFunction
867
+ * @param {ComponentContext} ctx - The component context
868
+ * @returns {string} CSS styles string
869
+ */
870
+
871
+ /**
872
+ * @typedef {Record<string, ComponentDefinition|string>} ChildrenMap
873
+ * Map of CSS selectors to component definitions or registered component names
874
+ */
875
+
876
+ // -----------------------------------------------------------------------------
877
+ // Context Types
878
+ // -----------------------------------------------------------------------------
879
+
880
+ /**
881
+ * @typedef {Object} ComponentContext
882
+ * @property {ComponentProps} props
883
+ * Component properties passed during mounting
884
+ * @property {Emitter} emitter
885
+ * Event emitter instance for component event handling
886
+ * @property {SignalFactory} signal
887
+ * Factory function to create reactive Signal instances
888
+ */
889
+
890
+ /**
891
+ * @typedef {Record<string, unknown>} ComponentProps
892
+ * Properties passed to a component during mounting
893
+ */
894
+
895
+ /**
896
+ * @callback SignalFactory
897
+ * @template T
898
+ * @param {T} initialValue - The initial value for the signal
899
+ * @returns {Signal<T>} A new Signal instance
900
+ */
901
+
902
+ // -----------------------------------------------------------------------------
903
+ // Lifecycle Hook Types
904
+ // -----------------------------------------------------------------------------
905
+
906
+ /**
907
+ * @typedef {Object} LifecycleHooks
908
+ * @property {LifecycleHook} [onBeforeMount]
909
+ * Hook called before component mounting
910
+ * @property {LifecycleHook} [onMount]
911
+ * Hook called after component mounting
912
+ * @property {LifecycleHook} [onBeforeUpdate]
913
+ * Hook called before component update
914
+ * @property {LifecycleHook} [onUpdate]
915
+ * Hook called after component update
916
+ * @property {UnmountHook} [onUnmount]
917
+ * Hook called during component unmounting
918
+ */
919
+
920
+ /**
921
+ * @callback LifecycleHook
922
+ * @param {LifecycleHookContext} ctx - Context with container and component data
923
+ * @returns {void|Promise<void>}
924
+ */
925
+
926
+ /**
927
+ * @callback UnmountHook
928
+ * @param {UnmountHookContext} ctx - Context with cleanup resources
929
+ * @returns {void|Promise<void>}
930
+ */
931
+
932
+ /**
933
+ * @typedef {Object} LifecycleHookContext
934
+ * @property {HTMLElement} container
935
+ * The DOM element where the component is mounted
936
+ * @property {ComponentContext & SetupResult} context
937
+ * The component's reactive state and context data
938
+ */
939
+
940
+ /**
941
+ * @typedef {Object} UnmountHookContext
942
+ * @property {HTMLElement} container
943
+ * The DOM element where the component is mounted
944
+ * @property {ComponentContext & SetupResult} context
945
+ * The component's reactive state and context data
946
+ * @property {CleanupResources} cleanup
947
+ * Object containing cleanup functions and instances
948
+ */
949
+
257
950
  /**
258
- * 🧩 Eleva Core: Signal-based component runtime framework with lifecycle, scoped styles, and plugins.
951
+ * @typedef {Object} CleanupResources
952
+ * @property {Array<UnsubscribeFunction>} watchers
953
+ * Signal watcher cleanup functions
954
+ * @property {Array<UnsubscribeFunction>} listeners
955
+ * Event listener cleanup functions
956
+ * @property {Array<MountResult>} children
957
+ * Child component instances
958
+ */
959
+
960
+ // -----------------------------------------------------------------------------
961
+ // Mount Result Types
962
+ // -----------------------------------------------------------------------------
963
+
964
+ /**
965
+ * @typedef {Object} MountResult
966
+ * @property {HTMLElement} container
967
+ * The DOM element where the component is mounted
968
+ * @property {ComponentContext & SetupResult} data
969
+ * The component's reactive state and context data
970
+ * @property {UnmountFunction} unmount
971
+ * Function to clean up and unmount the component
972
+ */
973
+
974
+ /**
975
+ * @callback UnmountFunction
976
+ * @returns {Promise<void>}
977
+ */
978
+
979
+ /**
980
+ * @callback UnsubscribeFunction
981
+ * @returns {void|boolean}
982
+ */
983
+
984
+ // -----------------------------------------------------------------------------
985
+ // Plugin Types
986
+ // -----------------------------------------------------------------------------
987
+
988
+ /**
989
+ * @typedef {Object} ElevaPlugin
990
+ * @property {PluginInstallFunction} install
991
+ * Function that installs the plugin into the Eleva instance
992
+ * @property {string} name
993
+ * Unique identifier name for the plugin
994
+ * @property {PluginUninstallFunction} [uninstall]
995
+ * Optional function to uninstall the plugin
996
+ */
997
+
998
+ /**
999
+ * @callback PluginInstallFunction
1000
+ * @param {Eleva} eleva - The Eleva instance
1001
+ * @param {PluginOptions} options - Plugin configuration options
1002
+ * @returns {void|Eleva|unknown} Optionally returns the Eleva instance or plugin result
1003
+ */
1004
+
1005
+ /**
1006
+ * @callback PluginUninstallFunction
1007
+ * @param {Eleva} eleva - The Eleva instance
1008
+ * @returns {void}
1009
+ */
1010
+
1011
+ /**
1012
+ * @typedef {Record<string, unknown>} PluginOptions
1013
+ * Configuration options passed to a plugin during installation
1014
+ */
1015
+
1016
+ // -----------------------------------------------------------------------------
1017
+ // Event Types
1018
+ // -----------------------------------------------------------------------------
1019
+
1020
+ /**
1021
+ * @callback EventHandler
1022
+ * @param {Event} event - The DOM event object
1023
+ * @returns {void}
1024
+ */
1025
+
1026
+ /**
1027
+ * @typedef {'click'|'submit'|'input'|'change'|'focus'|'blur'|'keydown'|'keyup'|'keypress'|'mouseenter'|'mouseleave'|'mouseover'|'mouseout'|'mousedown'|'mouseup'|'touchstart'|'touchend'|'touchmove'|'scroll'|'resize'|'load'|'error'|string} DOMEventName
1028
+ * Common DOM event names (prefixed with @ in templates)
1029
+ */
1030
+
1031
+ /**
1032
+ * @class 🧩 Eleva
1033
+ * @classdesc A modern, signal-based component runtime framework that provides lifecycle hooks,
1034
+ * scoped styles, and plugin support. Eleva manages component registration, plugin integration,
1035
+ * event handling, and DOM rendering with a focus on performance and developer experience.
259
1036
  *
260
- * The Eleva class is the core of the framework. It manages component registration,
261
- * plugin integration, lifecycle hooks, event handling, and DOM rendering.
1037
+ * @example
1038
+ * // Basic component creation and mounting
1039
+ * const app = new Eleva("myApp");
1040
+ * app.component("myComponent", {
1041
+ * setup: (ctx) => ({ count: ctx.signal(0) }),
1042
+ * template: (ctx) => `<div>Hello ${ctx.props.name}</div>`
1043
+ * });
1044
+ * app.mount(document.getElementById("app"), "myComponent", { name: "World" });
1045
+ *
1046
+ * @example
1047
+ * // Using lifecycle hooks
1048
+ * app.component("lifecycleDemo", {
1049
+ * setup: () => {
1050
+ * return {
1051
+ * onMount: ({ container, context }) => {
1052
+ * console.log('Component mounted!');
1053
+ * }
1054
+ * };
1055
+ * },
1056
+ * template: `<div>Lifecycle Demo</div>`
1057
+ * });
262
1058
  */
263
1059
  class Eleva {
264
1060
  /**
265
- * Creates a new Eleva instance.
1061
+ * Creates a new Eleva instance with the specified name and configuration.
1062
+ *
1063
+ * @public
1064
+ * @param {string} name - The unique identifier name for this Eleva instance.
1065
+ * @param {Record<string, unknown>} [config={}] - Optional configuration object for the instance.
1066
+ * May include framework-wide settings and default behaviors.
1067
+ * @throws {Error} If the name is not provided or is not a string.
1068
+ * @returns {Eleva} A new Eleva instance.
1069
+ *
1070
+ * @example
1071
+ * const app = new Eleva("myApp");
1072
+ * app.component("myComponent", {
1073
+ * setup: (ctx) => ({ count: ctx.signal(0) }),
1074
+ * template: (ctx) => `<div>Hello ${ctx.props.name}!</div>`
1075
+ * });
1076
+ * app.mount(document.getElementById("app"), "myComponent", { name: "World" });
266
1077
  *
267
- * @param {string} name - The name of the Eleva instance.
268
- * @param {object} [config={}] - Optional configuration for the instance.
269
1078
  */
270
1079
  constructor(name, config = {}) {
1080
+ /** @public {string} The unique identifier name for this Eleva instance */
271
1081
  this.name = name;
1082
+ /** @public {Object<string, unknown>} Optional configuration object for the Eleva instance */
272
1083
  this.config = config;
273
- this._components = {};
274
- this._plugins = [];
275
- this._lifecycleHooks = ["onBeforeMount", "onMount", "onBeforeUpdate", "onUpdate", "onUnmount"];
276
- this._isMounted = false;
1084
+ /** @public {Emitter} Instance of the event emitter for handling component events */
277
1085
  this.emitter = new Emitter();
1086
+ /** @public {typeof Signal} Static reference to the Signal class for creating reactive state */
1087
+ this.signal = Signal;
1088
+ /** @public {typeof TemplateEngine} Static reference to the TemplateEngine class for template parsing */
1089
+ this.templateEngine = TemplateEngine;
1090
+ /** @public {Renderer} Instance of the renderer for handling DOM updates and patching */
278
1091
  this.renderer = new Renderer();
1092
+
1093
+ /** @private {Map<string, ComponentDefinition>} Registry of all component definitions by name */
1094
+ this._components = new Map();
1095
+ /** @private {Map<string, ElevaPlugin>} Collection of installed plugin instances by name */
1096
+ this._plugins = new Map();
1097
+ /** @private {number} Counter for generating unique component IDs */
1098
+ this._componentCounter = 0;
279
1099
  }
280
1100
 
281
1101
  /**
282
1102
  * Integrates a plugin with the Eleva framework.
1103
+ * The plugin's install function will be called with the Eleva instance and provided options.
1104
+ * After installation, the plugin will be available for use by components.
1105
+ *
1106
+ * Note: Plugins that wrap core methods (e.g., mount) must be uninstalled in reverse order
1107
+ * of installation (LIFO - Last In, First Out) to avoid conflicts.
1108
+ *
1109
+ * @public
1110
+ * @param {ElevaPlugin} plugin - The plugin object which must have an `install` function.
1111
+ * @param {Object<string, unknown>} [options={}] - Optional configuration options for the plugin.
1112
+ * @returns {Eleva} The Eleva instance (for method chaining).
1113
+ * @example
1114
+ * app.use(myPlugin, { option1: "value1" });
283
1115
  *
284
- * @param {object} [plugin] - The plugin object which should have an install function.
285
- * @param {object} [options={}] - Optional options to pass to the plugin.
286
- * @returns {Eleva} The Eleva instance (for chaining).
1116
+ * @example
1117
+ * // Correct uninstall order (LIFO)
1118
+ * app.use(PluginA);
1119
+ * app.use(PluginB);
1120
+ * // Uninstall in reverse order:
1121
+ * PluginB.uninstall(app);
1122
+ * PluginA.uninstall(app);
287
1123
  */
288
1124
  use(plugin, options = {}) {
289
- if (typeof plugin.install === "function") {
290
- plugin.install(this, options);
291
- }
292
- this._plugins.push(plugin);
293
- return this;
1125
+ this._plugins.set(plugin.name, plugin);
1126
+ const result = plugin.install(this, options);
1127
+ return result !== undefined ? result : this;
294
1128
  }
295
1129
 
296
1130
  /**
297
- * Registers a component with the Eleva instance.
1131
+ * Registers a new component with the Eleva instance.
1132
+ * The component will be available for mounting using its registered name.
298
1133
  *
299
- * @param {string} name - The name of the component.
300
- * @param {object} definition - The component definition including setup, template, style, and children.
301
- * @returns {Eleva} The Eleva instance (for chaining).
1134
+ * @public
1135
+ * @param {string} name - The unique name of the component to register.
1136
+ * @param {ComponentDefinition} definition - The component definition including setup, template, style, and children.
1137
+ * @returns {Eleva} The Eleva instance (for method chaining).
1138
+ * @throws {Error} If the component name is already registered.
1139
+ * @example
1140
+ * app.component("myButton", {
1141
+ * template: (ctx) => `<button>${ctx.props.text}</button>`,
1142
+ * style: `button { color: blue; }`
1143
+ * });
302
1144
  */
303
1145
  component(name, definition) {
304
- this._components[name] = definition;
1146
+ /** @type {Map<string, ComponentDefinition>} */
1147
+ this._components.set(name, definition);
305
1148
  return this;
306
1149
  }
307
1150
 
308
1151
  /**
309
1152
  * Mounts a registered component to a DOM element.
1153
+ * This will initialize the component, set up its reactive state, and render it to the DOM.
310
1154
  *
311
- * @param {string|HTMLElement} selectorOrElement - A CSS selector string or DOM element where the component will be mounted.
312
- * @param {string} compName - The name of the component to mount.
313
- * @param {object} [props={}] - Optional properties to pass to the component.
314
- * @returns {object|Promise<object>} An object representing the mounted component instance, or a Promise that resolves to it for asynchronous setups.
315
- * @throws Will throw an error if the container or component is not found.
1155
+ * @public
1156
+ * @param {HTMLElement} container - The DOM element where the component will be mounted.
1157
+ * @param {string|ComponentDefinition} compName - The name of the registered component or a direct component definition.
1158
+ * @param {Object<string, unknown>} [props={}] - Optional properties to pass to the component.
1159
+ * @returns {Promise<MountResult>}
1160
+ * A Promise that resolves to an object containing:
1161
+ * - container: The mounted component's container element
1162
+ * - data: The component's reactive state and context
1163
+ * - unmount: Function to clean up and unmount the component
1164
+ * @throws {Error} If the container is not found, or component is not registered.
1165
+ * @example
1166
+ * const instance = await app.mount(document.getElementById("app"), "myComponent", { text: "Click me" });
1167
+ * // Later...
1168
+ * instance.unmount();
316
1169
  */
317
- mount(selectorOrElement, compName, props = {}) {
318
- const container = typeof selectorOrElement === "string" ? document.querySelector(selectorOrElement) : selectorOrElement;
319
- if (!container) throw new Error(`Container not found: ${selectorOrElement}`);
320
- const definition = this._components[compName];
1170
+ async mount(container, compName, props = {}) {
1171
+ if (!container) throw new Error(`Container not found: ${container}`);
1172
+ if (container._eleva_instance) return container._eleva_instance;
1173
+
1174
+ /** @type {ComponentDefinition} */
1175
+ const definition = typeof compName === "string" ? this._components.get(compName) : compName;
321
1176
  if (!definition) throw new Error(`Component "${compName}" not registered.`);
1177
+
1178
+ /** @type {string} */
1179
+ const compId = `c${++this._componentCounter}`;
1180
+
1181
+ /**
1182
+ * Destructure the component definition to access core functionality.
1183
+ * - setup: Optional function for component initialization and state management
1184
+ * - template: Required function or string that returns the component's HTML structure
1185
+ * - style: Optional function or string for component-scoped CSS styles
1186
+ * - children: Optional object defining nested child components
1187
+ */
322
1188
  const {
323
1189
  setup,
324
1190
  template,
325
1191
  style,
326
1192
  children
327
1193
  } = definition;
1194
+
1195
+ /** @type {ComponentContext} */
328
1196
  const context = {
329
1197
  props,
330
- emit: this.emitter.emit.bind(this.emitter),
331
- on: this.emitter.on.bind(this.emitter),
332
- signal: v => new Signal(v),
333
- ...this._prepareLifecycleHooks()
1198
+ emitter: this.emitter,
1199
+ /** @type {(v: unknown) => Signal<unknown>} */
1200
+ signal: v => new this.signal(v)
334
1201
  };
335
1202
 
336
1203
  /**
337
1204
  * Processes the mounting of the component.
1205
+ * This function handles:
1206
+ * 1. Merging setup data with the component context
1207
+ * 2. Setting up reactive watchers
1208
+ * 3. Rendering the component
1209
+ * 4. Managing component lifecycle
338
1210
  *
339
- * @param {object} data - Data returned from the component's setup function.
340
- * @returns {object} An object with the container, merged context data, and an unmount function.
1211
+ * @param {Object<string, unknown>} data - Data returned from the component's setup function
1212
+ * @returns {Promise<MountResult>} An object containing:
1213
+ * - container: The mounted component's container element
1214
+ * - data: The component's reactive state and context
1215
+ * - unmount: Function to clean up and unmount the component
341
1216
  */
342
- const processMount = data => {
1217
+ const processMount = async data => {
1218
+ /** @type {ComponentContext} */
343
1219
  const mergedContext = {
344
1220
  ...context,
345
1221
  ...data
346
1222
  };
347
- const watcherUnsubscribers = [];
1223
+ /** @type {Array<() => void>} */
1224
+ const watchers = [];
1225
+ /** @type {Array<MountResult>} */
348
1226
  const childInstances = [];
349
- if (!this._isMounted) {
350
- mergedContext.onBeforeMount && mergedContext.onBeforeMount();
351
- } else {
352
- mergedContext.onBeforeUpdate && mergedContext.onBeforeUpdate();
353
- }
1227
+ /** @type {Array<() => void>} */
1228
+ const listeners = [];
1229
+ /** @private {boolean} Local mounted state for this component instance */
1230
+ let isMounted = false;
354
1231
 
355
1232
  /**
356
- * Renders the component by parsing the template, patching the DOM,
357
- * processing events, injecting styles, and mounting child components.
1233
+ * Renders the component by:
1234
+ * 1. Executing lifecycle hooks
1235
+ * 2. Processing the template
1236
+ * 3. Updating the DOM
1237
+ * 4. Processing events, injecting styles, and mounting child components.
358
1238
  */
359
- const render = () => {
360
- const newHtml = TemplateEngine.parse(template(mergedContext), mergedContext);
1239
+ const render = async () => {
1240
+ // Execute before hooks
1241
+ if (!isMounted) {
1242
+ await mergedContext.onBeforeMount?.({
1243
+ container,
1244
+ context: mergedContext
1245
+ });
1246
+ } else {
1247
+ await mergedContext.onBeforeUpdate?.({
1248
+ container,
1249
+ context: mergedContext
1250
+ });
1251
+ }
1252
+ const templateResult = typeof template === "function" ? await template(mergedContext) : template;
1253
+ const newHtml = this.templateEngine.parse(templateResult, mergedContext);
361
1254
  this.renderer.patchDOM(container, newHtml);
362
- this._processEvents(container, mergedContext);
363
- this._injectStyles(container, compName, style, mergedContext);
364
- this._mountChildren(container, children, childInstances);
365
- if (!this._isMounted) {
366
- mergedContext.onMount && mergedContext.onMount();
367
- this._isMounted = true;
1255
+ this._processEvents(container, mergedContext, listeners);
1256
+ if (style) this._injectStyles(container, compId, style, mergedContext);
1257
+ if (children) await this._mountComponents(container, children, childInstances);
1258
+
1259
+ // Execute after hooks
1260
+ if (!isMounted) {
1261
+ await mergedContext.onMount?.({
1262
+ container,
1263
+ context: mergedContext
1264
+ });
1265
+ isMounted = true;
368
1266
  } else {
369
- mergedContext.onUpdate && mergedContext.onUpdate();
1267
+ await mergedContext.onUpdate?.({
1268
+ container,
1269
+ context: mergedContext
1270
+ });
370
1271
  }
371
1272
  };
372
- Object.values(data).forEach(val => {
373
- if (val instanceof Signal) watcherUnsubscribers.push(val.watch(render));
374
- });
375
- render();
376
- return {
1273
+
1274
+ /**
1275
+ * Sets up reactive watchers for all Signal instances in the component's data.
1276
+ * When a Signal's value changes, the component will re-render to reflect the updates.
1277
+ * Stores unsubscribe functions to clean up watchers when component unmounts.
1278
+ */
1279
+ for (const val of Object.values(data)) {
1280
+ if (val instanceof Signal) watchers.push(val.watch(render));
1281
+ }
1282
+ await render();
1283
+ const instance = {
377
1284
  container,
378
1285
  data: mergedContext,
379
1286
  /**
380
- * Unmounts the component, cleaning up watchers, child components, and clearing the container.
1287
+ * Unmounts the component, cleaning up watchers and listeners, child components, and clearing the container.
1288
+ *
1289
+ * @returns {void}
381
1290
  */
382
- unmount: () => {
383
- watcherUnsubscribers.forEach(fn => fn());
384
- childInstances.forEach(child => child.unmount());
385
- mergedContext.onUnmount && mergedContext.onUnmount();
1291
+ unmount: async () => {
1292
+ /** @type {UnmountHookContext} */
1293
+ await mergedContext.onUnmount?.({
1294
+ container,
1295
+ context: mergedContext,
1296
+ cleanup: {
1297
+ watchers: watchers,
1298
+ listeners: listeners,
1299
+ children: childInstances
1300
+ }
1301
+ });
1302
+ for (const fn of watchers) fn();
1303
+ for (const fn of listeners) fn();
1304
+ for (const child of childInstances) await child.unmount();
386
1305
  container.innerHTML = "";
1306
+ delete container._eleva_instance;
387
1307
  }
388
1308
  };
1309
+ container._eleva_instance = instance;
1310
+ return instance;
389
1311
  };
390
1312
 
391
- // Handle asynchronous setup if needed.
392
- const setupResult = setup(context);
393
- if (setupResult && typeof setupResult.then === "function") {
394
- return setupResult.then(data => processMount(data));
395
- } else {
396
- const data = setupResult || {};
397
- return processMount(data);
398
- }
1313
+ // Handle asynchronous setup.
1314
+ const setupResult = typeof setup === "function" ? await setup(context) : {};
1315
+ return await processMount(setupResult);
399
1316
  }
400
1317
 
401
1318
  /**
402
- * Prepares default no-operation lifecycle hook functions.
1319
+ * Processes DOM elements for event binding based on attributes starting with "@".
1320
+ * This method handles the event delegation system and ensures proper cleanup of event listeners.
403
1321
  *
404
- * @returns {object} An object with keys for lifecycle hooks mapped to empty functions.
405
1322
  * @private
1323
+ * @param {HTMLElement} container - The container element in which to search for event attributes.
1324
+ * @param {ComponentContext} context - The current component context containing event handler definitions.
1325
+ * @param {Array<() => void>} listeners - Array to collect cleanup functions for each event listener.
1326
+ * @returns {void}
406
1327
  */
407
- _prepareLifecycleHooks() {
408
- return this._lifecycleHooks.reduce((acc, hook) => {
409
- acc[hook] = () => {};
410
- return acc;
411
- }, {});
1328
+ _processEvents(container, context, listeners) {
1329
+ /** @type {NodeListOf<Element>} */
1330
+ const elements = container.querySelectorAll("*");
1331
+ for (const el of elements) {
1332
+ /** @type {NamedNodeMap} */
1333
+ const attrs = el.attributes;
1334
+ for (let i = 0; i < attrs.length; i++) {
1335
+ /** @type {Attr} */
1336
+ const attr = attrs[i];
1337
+ if (!attr.name.startsWith("@")) continue;
1338
+
1339
+ /** @type {keyof HTMLElementEventMap} */
1340
+ const event = attr.name.slice(1);
1341
+ /** @type {string} */
1342
+ const handlerName = attr.value;
1343
+ /** @type {(event: Event) => void} */
1344
+ const handler = context[handlerName] || this.templateEngine.evaluate(handlerName, context);
1345
+ if (typeof handler === "function") {
1346
+ el.addEventListener(event, handler);
1347
+ el.removeAttribute(attr.name);
1348
+ listeners.push(() => el.removeEventListener(event, handler));
1349
+ }
1350
+ }
1351
+ }
412
1352
  }
413
1353
 
414
1354
  /**
415
- * Processes DOM elements for event binding based on attributes starting with "@".
1355
+ * Injects scoped styles into the component's container.
1356
+ * The styles are automatically prefixed to prevent style leakage to other components.
416
1357
  *
417
- * @param {HTMLElement} container - The container element in which to search for events.
418
- * @param {object} context - The current context containing event handler definitions.
419
1358
  * @private
1359
+ * @param {HTMLElement} container - The container element where styles should be injected.
1360
+ * @param {string} compId - The component ID used to identify the style element.
1361
+ * @param {(function(ComponentContext): string)|string} styleDef - The component's style definition (function or string).
1362
+ * @param {ComponentContext} context - The current component context for style interpolation.
1363
+ * @returns {void}
420
1364
  */
421
- _processEvents(container, context) {
422
- container.querySelectorAll("*").forEach(el => {
423
- [...el.attributes].forEach(({
424
- name,
425
- value
426
- }) => {
427
- if (name.startsWith("@")) {
428
- const event = name.slice(1);
429
- const handler = TemplateEngine.evaluate(value, context);
430
- if (typeof handler === "function") {
431
- el.addEventListener(event, handler);
432
- el.removeAttribute(name);
433
- }
434
- }
435
- });
436
- });
1365
+ _injectStyles(container, compId, styleDef, context) {
1366
+ /** @type {string} */
1367
+ const newStyle = typeof styleDef === "function" ? this.templateEngine.parse(styleDef(context), context) : styleDef;
1368
+
1369
+ /** @type {HTMLStyleElement|null} */
1370
+ let styleEl = container.querySelector(`style[data-e-style="${compId}"]`);
1371
+ if (styleEl && styleEl.textContent === newStyle) return;
1372
+ if (!styleEl) {
1373
+ styleEl = document.createElement("style");
1374
+ styleEl.setAttribute("data-e-style", compId);
1375
+ container.appendChild(styleEl);
1376
+ }
1377
+ styleEl.textContent = newStyle;
437
1378
  }
438
1379
 
439
1380
  /**
440
- * Injects scoped styles into the component's container.
1381
+ * Extracts props from an element's attributes that start with the specified prefix.
1382
+ * This method is used to collect component properties from DOM elements.
441
1383
  *
442
- * @param {HTMLElement} container - The container element.
443
- * @param {string} compName - The component name used to identify the style element.
444
- * @param {Function} styleFn - A function that returns CSS styles as a string.
445
- * @param {object} context - The current context for style interpolation.
446
1384
  * @private
1385
+ * @param {HTMLElement} element - The DOM element to extract props from
1386
+ * @returns {Record<string, string>} An object containing the extracted props
1387
+ * @example
1388
+ * // For an element with attributes:
1389
+ * // <div :name="John" :age="25">
1390
+ * // Returns: { name: "John", age: "25" }
447
1391
  */
448
- _injectStyles(container, compName, styleFn, context) {
449
- if (styleFn) {
450
- let styleEl = container.querySelector(`style[data-eleva-style="${compName}"]`);
451
- if (!styleEl) {
452
- styleEl = document.createElement("style");
453
- styleEl.setAttribute("data-eleva-style", compName);
454
- container.appendChild(styleEl);
1392
+ _extractProps(element) {
1393
+ if (!element.attributes) return {};
1394
+ const props = {};
1395
+ const attrs = element.attributes;
1396
+ for (let i = attrs.length - 1; i >= 0; i--) {
1397
+ const attr = attrs[i];
1398
+ if (attr.name.startsWith(":")) {
1399
+ const propName = attr.name.slice(1);
1400
+ props[propName] = attr.value;
1401
+ element.removeAttribute(attr.name);
455
1402
  }
456
- styleEl.textContent = TemplateEngine.parse(styleFn(context), context);
457
1403
  }
1404
+ return props;
458
1405
  }
459
1406
 
460
1407
  /**
461
- * Mounts child components within the parent component's container.
1408
+ * Mounts all components within the parent component's container.
1409
+ * This method handles mounting of explicitly defined children components.
1410
+ *
1411
+ * The mounting process follows these steps:
1412
+ * 1. Cleans up any existing component instances
1413
+ * 2. Mounts explicitly defined children components
462
1414
  *
463
- * @param {HTMLElement} container - The parent container element.
464
- * @param {object} children - An object mapping child component selectors to their definitions.
465
- * @param {Array} childInstances - An array to store the mounted child component instances.
466
1415
  * @private
1416
+ * @param {HTMLElement} container - The container element to mount components in
1417
+ * @param {Object<string, ComponentDefinition>} children - Map of selectors to component definitions for explicit children
1418
+ * @param {Array<MountResult>} childInstances - Array to store all mounted component instances
1419
+ * @returns {Promise<void>}
1420
+ *
1421
+ * @example
1422
+ * // Explicit children mounting:
1423
+ * const children = {
1424
+ * 'UserProfile': UserProfileComponent,
1425
+ * '#settings-panel': "settings-panel"
1426
+ * };
467
1427
  */
468
- _mountChildren(container, children, childInstances) {
469
- childInstances.forEach(child => child.unmount());
470
- childInstances.length = 0;
471
- Object.keys(children || {}).forEach(childName => {
472
- container.querySelectorAll(childName).forEach(childEl => {
473
- const props = {};
474
- [...childEl.attributes].forEach(({
475
- name,
476
- value
477
- }) => {
478
- if (name.startsWith("eleva-prop-")) {
479
- props[name.slice("eleva-prop-".length)] = value;
480
- }
481
- });
482
- const instance = this.mount(childEl, childName, props);
483
- childInstances.push(instance);
484
- });
485
- });
1428
+ async _mountComponents(container, children, childInstances) {
1429
+ for (const [selector, component] of Object.entries(children)) {
1430
+ if (!selector) continue;
1431
+ for (const el of container.querySelectorAll(selector)) {
1432
+ if (!(el instanceof HTMLElement)) continue;
1433
+ /** @type {Record<string, string>} */
1434
+ const props = this._extractProps(el);
1435
+ /** @type {MountResult} */
1436
+ const instance = await this.mount(el, component, props);
1437
+ if (instance && !childInstances.includes(instance)) {
1438
+ childInstances.push(instance);
1439
+ }
1440
+ }
1441
+ }
486
1442
  }
487
1443
  }
488
1444