@unsetsoft/ryunixjs 1.2.2 → 1.2.3-canary.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.
package/dist/Ryunix.js CHANGED
@@ -1,105 +1,115 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('lodash')) :
3
- typeof define === 'function' && define.amd ? define(['exports', 'lodash'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Ryunix = {}, global.lodash));
5
- })(this, (function (exports, lodash) { 'use strict';
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Ryunix = {}));
5
+ })(this, (function (exports) { 'use strict';
6
6
 
7
- let vars = {
7
+ // Improved state management - avoid global mutable object
8
+ // Instead, create a state manager that can be instantiated per render tree
9
+
10
+ const createRenderState = () => ({
8
11
  containerRoot: null,
9
12
  nextUnitOfWork: null,
10
13
  currentRoot: null,
11
14
  wipRoot: null,
12
- deletions: null,
15
+ deletions: [],
13
16
  wipFiber: null,
14
- hookIndex: null,
15
- effects: null,
16
- };
17
+ hookIndex: 0,
18
+ effects: [],
19
+ });
17
20
 
18
- const reg = /[A-Z]/g;
21
+ // Singleton for backward compatibility, but allows testing with isolated instances
22
+ let globalState = createRenderState();
23
+
24
+ const getState = () => globalState;
25
+
26
+ // Use const for regex to prevent accidental modification
27
+ const CAMEL_TO_KEBAB_REGEX = /[A-Z]/g;
19
28
 
20
29
  const RYUNIX_TYPES = Object.freeze({
21
- TEXT_ELEMENT: Symbol('text.element').toString(),
22
- Ryunix_ELEMENT: Symbol('ryunix.element').toString(),
23
- RYUNIX_EFFECT: Symbol('ryunix.effect').toString(),
24
- RYUNIX_MEMO: Symbol('ryunix.memo').toString(),
25
- RYUNIX_URL_QUERY: Symbol('ryunix.urlQuery').toString(),
26
- RYUNIX_REF: Symbol('ryunix.ref').toString(),
27
- RYUNIX_STORE: Symbol('ryunix.store').toString(),
28
- RYUNIX_REDUCE: Symbol('ryunix.reduce').toString(),
29
- RYUNIX_FRAGMENT: Symbol('ryunix.fragment').toString(),
30
- RYUNIX_CONTEXT: Symbol('ryunix.context').toString(),
30
+ TEXT_ELEMENT: Symbol.for('ryunix.text.element'),
31
+ RYUNIX_ELEMENT: Symbol.for('ryunix.element'),
32
+ RYUNIX_EFFECT: Symbol.for('ryunix.effect'),
33
+ RYUNIX_MEMO: Symbol.for('ryunix.memo'),
34
+ RYUNIX_URL_QUERY: Symbol.for('ryunix.urlQuery'),
35
+ RYUNIX_REF: Symbol.for('ryunix.ref'),
36
+ RYUNIX_STORE: Symbol.for('ryunix.store'),
37
+ RYUNIX_REDUCE: Symbol.for('ryunix.reduce'),
38
+ RYUNIX_FRAGMENT: Symbol.for('ryunix.fragment'),
39
+ RYUNIX_CONTEXT: Symbol.for('ryunix.context'),
31
40
  });
32
41
 
33
42
  const STRINGS = Object.freeze({
34
- object: 'object',
35
- function: 'function',
36
- style: 'ryunix-style',
37
- className: 'ryunix-class',
38
- children: 'children',
39
- boolean: 'boolean',
40
- string: 'string',
43
+ OBJECT: 'object',
44
+ FUNCTION: 'function',
45
+ STYLE: 'ryunix-style',
46
+ CLASS_NAME: 'ryunix-class',
47
+ CHILDREN: 'children',
48
+ BOOLEAN: 'boolean',
49
+ STRING: 'string',
50
+ UNDEFINED: 'undefined',
41
51
  });
42
52
 
43
53
  const OLD_STRINGS = Object.freeze({
44
- style: 'style',
45
- className: 'className',
54
+ STYLE: 'style',
55
+ CLASS_NAME: 'className',
46
56
  });
47
57
 
48
58
  const EFFECT_TAGS = Object.freeze({
49
- PLACEMENT: Symbol('ryunix.reconciler.status.placement').toString(),
50
- UPDATE: Symbol('ryunix.reconciler.status.update').toString(),
51
- DELETION: Symbol('ryunix.reconciler.status.deletion').toString(),
52
- NO_EFFECT: Symbol('ryunix.reconciler.status.no_efect').toString(),
59
+ PLACEMENT: Symbol.for('ryunix.reconciler.status.placement'),
60
+ UPDATE: Symbol.for('ryunix.reconciler.status.update'),
61
+ DELETION: Symbol.for('ryunix.reconciler.status.deletion'),
62
+ NO_EFFECT: Symbol.for('ryunix.reconciler.status.no_effect'),
53
63
  });
54
64
 
55
65
  /**
56
- * The function creates a new element with the given type, props, and children.
57
- * @param type - The type of the element to be created, such as "div", "span", "h1", etc.
58
- * @param props - The `props` parameter is an object that contains the properties or attributes of the
59
- * element being created. These properties can include things like `className`, `id`, `style`, and any
60
- * other custom attributes that the user wants to add to the element. The `props` object is spread
61
- * using the spread
62
- * @param children - The `children` parameter is a rest parameter that allows the function to accept
63
- * any number of arguments after the `props` parameter. These arguments will be treated as children
64
- * elements of the created element. The `map` function is used to iterate over each child and create a
65
- * new element if it is not
66
- * @returns A JavaScript object with a `type` property and a `props` property. The `type` property is
67
- * set to the `type` argument passed into the function, and the `props` property is an object that
68
- * includes any additional properties passed in the `props` argument, as well as a `children` property
69
- * that is an array of any child elements passed in the `...children` argument
66
+ * Type checking utilities
70
67
  */
68
+ const is = {
69
+ object: (val) => val !== null && typeof val === STRINGS.OBJECT,
70
+ function: (val) => typeof val === STRINGS.FUNCTION,
71
+ string: (val) => typeof val === STRINGS.STRING,
72
+ undefined: (val) => typeof val === STRINGS.UNDEFINED,
73
+ null: (val) => val === null,
74
+ array: (val) => Array.isArray(val),
75
+ promise: (val) => val instanceof Promise,
76
+ };
71
77
 
72
- const createElement = (type, props, ...children) => {
78
+ /**
79
+ * Create text element
80
+ */
81
+ const createTextElement = (text) => {
73
82
  return {
74
- type,
83
+ type: RYUNIX_TYPES.TEXT_ELEMENT,
75
84
  props: {
76
- ...props,
77
- children: children
78
- .flat()
79
- .map((child) =>
80
- typeof child === STRINGS.object ? child : createTextElement(child),
81
- ),
85
+ nodeValue: text,
86
+ children: [],
82
87
  },
83
88
  }
84
89
  };
85
90
 
86
91
  /**
87
- * The function creates a text element with a given text value.
88
- * @param text - The text content that will be used to create a new text element.
89
- * @returns A JavaScript object with a `type` property set to `"TEXT_ELEMENT"` and a `props` property
90
- * that contains a `nodeValue` property set to the `text` parameter and an empty `children` array.
92
+ * Create element
91
93
  */
94
+ const createElement = (type, props, ...children) => {
95
+ const safeProps = props || {};
92
96
 
93
- const createTextElement = (text) => {
94
97
  return {
95
- type: RYUNIX_TYPES.TEXT_ELEMENT,
98
+ type,
96
99
  props: {
97
- nodeValue: text,
98
- children: [],
100
+ ...safeProps,
101
+ children: children
102
+ .flat()
103
+ .map((child) =>
104
+ typeof child === STRINGS.OBJECT ? child : createTextElement(child),
105
+ ),
99
106
  },
100
107
  }
101
108
  };
102
109
 
110
+ /**
111
+ * Fragment component
112
+ */
103
113
  const Fragment = (props) => {
104
114
  const children = Array.isArray(props.children)
105
115
  ? props.children
@@ -107,214 +117,351 @@
107
117
  return createElement(RYUNIX_TYPES.RYUNIX_FRAGMENT, {}, ...children)
108
118
  };
109
119
 
120
+ /**
121
+ * Check if a key is an event handler
122
+ * @param {string} key - Prop key
123
+ * @returns {boolean}
124
+ */
110
125
  const isEvent = (key) => key.startsWith('on');
111
- const isProperty = (key) => key !== STRINGS.children && !isEvent(key);
112
- const isNew = (prev, next) => (key) => prev[key] !== next[key];
126
+
127
+ /**
128
+ * Check if a key is a property (not children or event)
129
+ * @param {string} key - Prop key
130
+ * @returns {boolean}
131
+ */
132
+ const isProperty = (key) => key !== STRINGS.CHILDREN && !isEvent(key);
133
+
134
+ /**
135
+ * Check if a property is new or changed
136
+ * @param {Object} prev - Previous props
137
+ * @param {Object} next - Next props
138
+ * @returns {Function}
139
+ */
140
+ const isNew = (prev, next) => (key) => {
141
+ // Use Object.is for better comparison (handles NaN, -0, +0)
142
+ return !Object.is(prev[key], next[key])
143
+ };
144
+
145
+ /**
146
+ * Check if a property was removed
147
+ * @param {Object} next - Next props
148
+ * @returns {Function}
149
+ */
113
150
  const isGone = (next) => (key) => !(key in next);
114
- const hasDepsChanged = (prevDeps, nextDeps) =>
115
- !prevDeps ||
116
- !nextDeps ||
117
- prevDeps.length !== nextDeps.length ||
118
- prevDeps.some((dep, index) => dep !== nextDeps[index]);
119
151
 
120
152
  /**
121
- * The function cancels all effect hooks in a given fiber.
122
- * @param fiber - The "fiber" parameter is likely referring to a data structure used in React.js to
123
- * represent a component and its state. It contains information about the component's props, state, and
124
- * children, as well as metadata used by React to manage updates and rendering. The function
125
- * "cancelEffects" is likely intended
153
+ * Cancel effects for a single fiber
154
+ * @param {Object} fiber - Fiber node
126
155
  */
127
156
  const cancelEffects = (fiber) => {
128
- if (fiber.hooks && fiber.hooks.length > 0) {
129
- fiber.hooks
130
- .filter((hook) => hook.type === RYUNIX_TYPES.RYUNIX_EFFECT && hook.cancel)
131
- .forEach((effectHook) => {
132
- effectHook.cancel();
133
- });
134
- }
157
+ if (!fiber?.hooks?.length) return
158
+
159
+ fiber.hooks
160
+ .filter(
161
+ (hook) =>
162
+ hook.type === RYUNIX_TYPES.RYUNIX_EFFECT && is.function(hook.cancel),
163
+ )
164
+ .forEach((hook) => {
165
+ try {
166
+ hook.cancel();
167
+ hook.cancel = null; // Clear reference to prevent memory leaks
168
+ } catch (error) {
169
+ if (process.env.NODE_ENV !== 'production') {
170
+ console.error('Error in effect cleanup:', error);
171
+ }
172
+ }
173
+ });
135
174
  };
136
175
 
137
176
  /**
138
- * The function `cancelEffectsDeep` recursively cancels effects in a fiber tree by running cleanup
139
- * functions for each effect hook.
140
- * @param fiber - The `fiber` parameter in the `cancelEffectsDeep` function seems to be an object
141
- * representing a fiber node in a data structure. The function recursively traverses the fiber tree to
142
- * find hooks with effects and cancels them by calling their `cancel` function if it exists. It also
143
- * logs a message
144
- * @returns The `cancelEffectsDeep` function does not explicitly return a value. It is a recursive
145
- * function that traverses a fiber tree structure and cancels effects for hooks that have a `cancel`
146
- * function defined. The function performs cleanup operations by calling the `cancel` function for each
147
- * applicable hook.
177
+ * Recursively cancel effects in fiber tree
178
+ * @param {Object} fiber - Root fiber node
148
179
  */
149
180
  const cancelEffectsDeep = (fiber) => {
150
181
  if (!fiber) return
151
182
 
152
- if (fiber.hooks && fiber.hooks.length > 0) {
183
+ // Cancel effects for current fiber
184
+ if (fiber.hooks?.length > 0) {
153
185
  fiber.hooks
154
186
  .filter(
155
187
  (hook) =>
156
- hook.type === RYUNIX_TYPES.RYUNIX_EFFECT &&
157
- typeof hook.cancel === STRINGS.function,
188
+ hook.type === RYUNIX_TYPES.RYUNIX_EFFECT && is.function(hook.cancel),
158
189
  )
159
190
  .forEach((hook) => {
160
- hook.cancel();
191
+ try {
192
+ hook.cancel();
193
+ hook.cancel = null; // Clear reference
194
+ } catch (error) {
195
+ if (process.env.NODE_ENV !== 'production') {
196
+ console.error('Error in deep effect cleanup:', error);
197
+ }
198
+ }
161
199
  });
162
200
  }
163
201
 
202
+ // Recursively process children
164
203
  if (fiber.child) cancelEffectsDeep(fiber.child);
165
204
  if (fiber.sibling) cancelEffectsDeep(fiber.sibling);
166
205
  };
167
206
 
168
207
  /**
169
- * The function runs all effect hooks in a given fiber.
170
- * @param fiber - The "fiber" parameter is likely referring to a data structure used in the
171
- * implementation of a fiber-based reconciliation algorithm, such as the one used in React. A fiber
172
- * represents a unit of work that needs to be performed by the reconciliation algorithm, and it
173
- * contains information about a component and its children, as
208
+ * Run effects for a fiber
209
+ * @param {Object} fiber - Fiber node
174
210
  */
175
211
  const runEffects = (fiber) => {
176
- if (!fiber.hooks || fiber.hooks.length === 0) return
212
+ if (!fiber?.hooks?.length) return
177
213
 
178
214
  for (let i = 0; i < fiber.hooks.length; i++) {
179
215
  const hook = fiber.hooks[i];
180
- if (
181
- hook.type === RYUNIX_TYPES.RYUNIX_EFFECT &&
182
- typeof hook.effect === STRINGS.function &&
183
- hook.effect !== null
184
- ) {
185
- if (typeof hook.cancel === STRINGS.function) {
186
- hook.cancel();
216
+
217
+ if (hook.type === RYUNIX_TYPES.RYUNIX_EFFECT && is.function(hook.effect)) {
218
+ // Cancel previous cleanup if exists
219
+ if (is.function(hook.cancel)) {
220
+ try {
221
+ hook.cancel();
222
+ } catch (error) {
223
+ if (process.env.NODE_ENV !== 'production') {
224
+ console.error('Error in effect cleanup:', error);
225
+ }
226
+ }
187
227
  }
188
228
 
189
- const cleanup = hook.effect();
229
+ // Run new effect
230
+ try {
231
+ const cleanup = hook.effect();
190
232
 
191
- if (typeof cleanup === 'function') {
192
- hook.cancel = cleanup;
193
- } else {
194
- hook.cancel = undefined;
233
+ // Store cleanup function if returned
234
+ if (is.function(cleanup)) {
235
+ hook.cancel = cleanup;
236
+ } else {
237
+ hook.cancel = null;
238
+ }
239
+ } catch (error) {
240
+ if (process.env.NODE_ENV !== 'production') {
241
+ console.error('Error in effect:', error);
242
+ }
243
+ hook.cancel = null;
195
244
  }
245
+
246
+ // Clear effect reference after running
247
+ hook.effect = null;
248
+ }
249
+ }
250
+ };
251
+
252
+ /**
253
+ * Convert camelCase to kebab-case for CSS properties
254
+ * @param {string} camelCase - CamelCase string
255
+ * @returns {string} Kebab-case string
256
+ */
257
+ const camelToKebab = (camelCase) => {
258
+ return camelCase.replace(
259
+ CAMEL_TO_KEBAB_REGEX,
260
+ (match) => `-${match.toLowerCase()}`,
261
+ )
262
+ };
263
+
264
+ /**
265
+ * Apply styles to DOM element
266
+ * @param {HTMLElement} dom - DOM element
267
+ * @param {Object} styleObj - Style object
268
+ */
269
+ const applyStyles = (dom, styleObj) => {
270
+ if (!is.object(styleObj) || is.null(styleObj)) {
271
+ dom.style.cssText = '';
272
+ return
273
+ }
274
+
275
+ try {
276
+ const cssText = Object.entries(styleObj)
277
+ .filter(([_, value]) => value != null) // Filter out null/undefined
278
+ .map(([key, value]) => {
279
+ const kebabKey = camelToKebab(key);
280
+ return `${kebabKey}: ${value}`
281
+ })
282
+ .join('; ');
283
+
284
+ dom.style.cssText = cssText;
285
+ } catch (error) {
286
+ if (process.env.NODE_ENV !== 'production') {
287
+ console.error('Error applying styles:', error);
288
+ }
289
+ }
290
+ };
291
+
292
+ /**
293
+ * Apply CSS classes to DOM element
294
+ * @param {HTMLElement} dom - DOM element
295
+ * @param {string} prevClasses - Previous class string
296
+ * @param {string} nextClasses - Next class string
297
+ */
298
+ const applyClasses = (dom, prevClasses, nextClasses) => {
299
+ if (!nextClasses) {
300
+ if (process.env.NODE_ENV !== 'production') {
301
+ throw new Error('className/ryunix-class cannot be empty')
196
302
  }
303
+ return
304
+ }
305
+
306
+ // Remove old classes
307
+ if (prevClasses) {
308
+ const oldClasses = prevClasses.split(/\s+/).filter(Boolean);
309
+ dom.classList.remove(...oldClasses);
310
+ }
311
+
312
+ // Add new classes
313
+ const newClasses = nextClasses.split(/\s+/).filter(Boolean);
314
+ if (newClasses.length > 0) {
315
+ dom.classList.add(...newClasses);
197
316
  }
198
317
  };
199
318
 
200
319
  /**
201
- * The function creates a new DOM element based on the given fiber object and updates its properties.
202
- * @param fiber - The fiber parameter is an object that represents a node in the fiber tree. It
203
- * contains information about the element type, props, and children of the node.
204
- * @returns The `createDom` function returns a newly created DOM element based on the `fiber` object
205
- * passed as an argument. If the `fiber` object represents a text element, a text node is created using
206
- * `document.createTextNode("")`. Otherwise, a new element is created using
207
- * `document.createElement(fiber.type)`. The function then calls the `updateDom` function to update the
208
- * properties of the newly created
320
+ * Create a DOM element from fiber
321
+ * @param {Object} fiber - Fiber node
322
+ * @returns {HTMLElement|Text|null}
209
323
  */
210
324
  const createDom = (fiber) => {
325
+ // Fragments don't create real DOM nodes
211
326
  if (fiber.type === RYUNIX_TYPES.RYUNIX_FRAGMENT) {
212
- return null // Los fragmentos no crean nodos DOM reales
327
+ return null
213
328
  }
214
- const dom =
215
- fiber.type == RYUNIX_TYPES.TEXT_ELEMENT
216
- ? document.createTextNode('')
217
- : document.createElement(fiber.type);
218
329
 
219
- updateDom(dom, {}, fiber.props);
330
+ let dom;
220
331
 
221
- return dom
332
+ try {
333
+ if (fiber.type === RYUNIX_TYPES.TEXT_ELEMENT) {
334
+ dom = document.createTextNode('');
335
+ } else if (is.string(fiber.type)) {
336
+ dom = document.createElement(fiber.type);
337
+ } else {
338
+ if (process.env.NODE_ENV !== 'production') {
339
+ console.warn(
340
+ 'Attempted to create DOM for non-host component:',
341
+ fiber.type,
342
+ );
343
+ }
344
+ return null
345
+ }
346
+
347
+ updateDom(dom, {}, fiber.props);
348
+ return dom
349
+ } catch (error) {
350
+ if (process.env.NODE_ENV !== 'production') {
351
+ console.error('Error creating DOM element:', error, fiber);
352
+ }
353
+ return null
354
+ }
222
355
  };
223
356
 
224
357
  /**
225
- * The function updates the DOM by removing old event listeners and properties, and adding new ones
226
- * based on the previous and next props.
227
- * @param dom - The DOM element that needs to be updated with new props.
228
- * @param prevProps - An object representing the previous props (properties) of a DOM element.
229
- * @param nextProps - An object containing the new props that need to be updated in the DOM.
358
+ * Update DOM element with new props
359
+ * @param {HTMLElement|Text} dom - DOM element
360
+ * @param {Object} prevProps - Previous props
361
+ * @param {Object} nextProps - Next props
230
362
  */
231
- const updateDom = (dom, prevProps, nextProps) => {
363
+ const updateDom = (dom, prevProps = {}, nextProps = {}) => {
364
+ // Remove old event listeners
232
365
  Object.keys(prevProps)
233
366
  .filter(isEvent)
234
367
  .filter((key) => isGone(nextProps)(key) || isNew(prevProps, nextProps)(key))
235
368
  .forEach((name) => {
236
369
  const eventType = name.toLowerCase().substring(2);
237
- dom.removeEventListener(eventType, prevProps[name]);
370
+ try {
371
+ dom.removeEventListener(eventType, prevProps[name]);
372
+ } catch (error) {
373
+ if (process.env.NODE_ENV !== 'production') {
374
+ console.warn('Error removing event listener:', error);
375
+ }
376
+ }
238
377
  });
239
378
 
379
+ // Remove old properties
240
380
  Object.keys(prevProps)
241
381
  .filter(isProperty)
242
382
  .filter(isGone(nextProps))
243
383
  .forEach((name) => {
384
+ // Skip special properties
385
+ if (
386
+ [
387
+ STRINGS.STYLE,
388
+ OLD_STRINGS.STYLE,
389
+ STRINGS.CLASS_NAME,
390
+ OLD_STRINGS.CLASS_NAME,
391
+ ].includes(name)
392
+ ) {
393
+ return
394
+ }
244
395
  dom[name] = '';
245
396
  });
246
397
 
398
+ // Set new properties
247
399
  Object.keys(nextProps)
248
400
  .filter(isProperty)
249
401
  .filter(isNew(prevProps, nextProps))
250
402
  .forEach((name) => {
251
- if (name === STRINGS.style) {
252
- DomStyle(dom, nextProps['ryunix-style']);
253
- } else if (name === OLD_STRINGS.style) {
254
- DomStyle(dom, nextProps.style);
255
- } else if (name === STRINGS.className) {
256
- if (nextProps['ryunix-class'] === '') {
257
- throw new Error('data-class cannot be empty.')
403
+ try {
404
+ // Handle style properties
405
+ if (name === STRINGS.STYLE || name === OLD_STRINGS.STYLE) {
406
+ const styleValue = nextProps[name];
407
+ applyStyles(dom, styleValue);
258
408
  }
259
-
260
- prevProps['ryunix-class'] &&
261
- dom.classList.remove(
262
- ...(prevProps['ryunix-class'].split(/\s+/).filter(Boolean) || []),
409
+ // Handle className properties
410
+ else if (name === STRINGS.CLASS_NAME) {
411
+ applyClasses(
412
+ dom,
413
+ prevProps[STRINGS.CLASS_NAME],
414
+ nextProps[STRINGS.CLASS_NAME],
263
415
  );
264
- dom.classList.add(
265
- ...nextProps['ryunix-class'].split(/\s+/).filter(Boolean),
266
- );
267
- } else if (name === OLD_STRINGS.className) {
268
- if (nextProps.className === '') {
269
- throw new Error('className cannot be empty.')
270
- }
271
-
272
- prevProps.className &&
273
- dom.classList.remove(
274
- ...(prevProps.className.split(/\s+/).filter(Boolean) || []),
416
+ } else if (name === OLD_STRINGS.CLASS_NAME) {
417
+ applyClasses(
418
+ dom,
419
+ prevProps[OLD_STRINGS.CLASS_NAME],
420
+ nextProps[OLD_STRINGS.CLASS_NAME],
275
421
  );
276
- dom.classList.add(...nextProps.className.split(/\s+/).filter(Boolean));
277
- } else {
278
- dom[name] = nextProps[name];
422
+ }
423
+ // Handle other properties
424
+ else {
425
+ // Special handling for value and checked (controlled components)
426
+ if (name === 'value' || name === 'checked') {
427
+ if (dom[name] !== nextProps[name]) {
428
+ dom[name] = nextProps[name];
429
+ }
430
+ } else {
431
+ dom[name] = nextProps[name];
432
+ }
433
+ }
434
+ } catch (error) {
435
+ if (process.env.NODE_ENV !== 'production') {
436
+ console.warn(`Error setting property ${name}:`, error);
437
+ }
279
438
  }
280
439
  });
281
440
 
441
+ // Add new event listeners
282
442
  Object.keys(nextProps)
283
443
  .filter(isEvent)
284
444
  .filter(isNew(prevProps, nextProps))
285
445
  .forEach((name) => {
286
446
  const eventType = name.toLowerCase().substring(2);
287
- dom.addEventListener(eventType, nextProps[name]);
288
- });
289
- };
290
-
291
- const DomStyle = (dom, style) => {
292
- dom.style = Object.keys(style).reduce((acc, styleName) => {
293
- const key = styleName.replace(reg, function (v) {
294
- return '-' + v.toLowerCase()
447
+ try {
448
+ dom.addEventListener(eventType, nextProps[name]);
449
+ } catch (error) {
450
+ if (process.env.NODE_ENV !== 'production') {
451
+ console.warn('Error adding event listener:', error);
452
+ }
453
+ }
295
454
  });
296
- acc += `${key}: ${style[styleName]};`;
297
- return acc
298
- }, '');
299
455
  };
300
456
 
301
- /**
302
- * The function commits changes made to the virtual DOM to the actual DOM.
303
- */
304
457
  const commitRoot = () => {
305
- vars.deletions.forEach(commitWork);
306
- commitWork(vars.wipRoot.child);
307
- vars.currentRoot = vars.wipRoot;
308
- vars.wipRoot = null;
458
+ const state = getState();
459
+ state.deletions.forEach(commitWork);
460
+ commitWork(state.wipRoot.child);
461
+ state.currentRoot = state.wipRoot;
462
+ state.wipRoot = null;
309
463
  };
310
464
 
311
- /**
312
- * The function commits changes made to the DOM based on the effect tag of the fiber.
313
- * @param fiber - A fiber is a unit of work in Ryunix's reconciliation process. It represents a
314
- * component and its state at a particular point in time. The `commitWork` function takes a fiber as a
315
- * parameter to commit the changes made during the reconciliation process to the actual DOM.
316
- * @returns The function does not return anything, it performs side effects by manipulating the DOM.
317
- */
318
465
  const commitWork = (fiber) => {
319
466
  if (!fiber) {
320
467
  return
@@ -347,13 +494,6 @@
347
494
  commitWork(fiber.sibling);
348
495
  };
349
496
 
350
- /**
351
- * The function removes a fiber's corresponding DOM node from its parent node or recursively removes
352
- * its child's DOM node until it finds a node to remove.
353
- * @param fiber - a fiber node in a fiber tree, which represents a component or an element in the Ryunix
354
- * application.
355
- * @param domParent - The parent DOM element from which the fiber's DOM element needs to be removed.
356
- */
357
497
  const commitDeletion = (fiber, domParent) => {
358
498
  if (fiber.dom) {
359
499
  domParent.removeChild(fiber.dom);
@@ -366,16 +506,8 @@
366
506
  }
367
507
  };
368
508
 
369
- /**
370
- * This function reconciles the children of a fiber node with a new set of elements, creating new
371
- * fibers for new elements, updating existing fibers for elements with the same type, and marking old
372
- * fibers for deletion if they are not present in the new set of elements.
373
- * @param wipFiber - A work-in-progress fiber object representing a component or element in the virtual
374
- * DOM tree.
375
- * @param elements - an array of elements representing the new children to be rendered in the current
376
- * fiber's subtree
377
- */
378
509
  const reconcileChildren = (wipFiber, elements) => {
510
+ const state = getState();
379
511
  let index = 0;
380
512
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
381
513
  let prevSibling;
@@ -409,7 +541,7 @@
409
541
  }
410
542
  if (oldFiber && !sameType) {
411
543
  oldFiber.effectTag = EFFECT_TAGS.DELETION;
412
- vars.deletions.push(oldFiber);
544
+ state.deletions.push(oldFiber);
413
545
  }
414
546
 
415
547
  if (oldFiber) {
@@ -427,20 +559,14 @@
427
559
  }
428
560
  };
429
561
 
430
- /**
431
- * This function updates a function component by setting up a work-in-progress fiber, resetting the
432
- * hook index, creating an empty hooks array, rendering the component, and reconciling its children.
433
- * @param fiber - The fiber parameter is an object that represents a node in the fiber tree. It
434
- * contains information about the component, its props, state, and children. In this function, it is
435
- * used to update the state of the component and its children.
436
- */
437
562
  const updateFunctionComponent = (fiber) => {
438
- vars.wipFiber = fiber;
439
- vars.hookIndex = 0;
440
- vars.wipFiber.hooks = [];
563
+ const state = getState();
564
+ state.wipFiber = fiber;
565
+ state.hookIndex = 0;
566
+ state.wipFiber.hooks = [];
567
+
441
568
  const children = [fiber.type(fiber.props)];
442
569
 
443
- // Aquí detectamos si es Provider para guardar contexto y valor en fiber
444
570
  if (fiber.type._contextId && fiber.props.value !== undefined) {
445
571
  fiber._contextId = fiber.type._contextId;
446
572
  fiber._contextValue = fiber.props.value;
@@ -449,62 +575,27 @@
449
575
  reconcileChildren(fiber, children);
450
576
  };
451
577
 
452
- /**
453
- * This function updates a host component's DOM element and reconciles its children.
454
- * @param fiber - A fiber is a unit of work in Ryunix that represents a component and its state. It
455
- * contains information about the component's type, props, and children, as well as pointers to other
456
- * fibers in the tree.
457
- */
458
578
  const updateHostComponent = (fiber) => {
459
- const children = Array.isArray(fiber.props.children)
460
- ? fiber.props.children.flat()
461
- : [fiber.props.children];
462
-
463
- if (fiber.type === RYUNIX_TYPES.RYUNIX_FRAGMENT) {
464
- reconcileChildren(fiber, children);
465
- } else {
466
- if (!fiber.dom) {
467
- fiber.dom = createDom(fiber);
468
- }
469
- reconcileChildren(fiber, children);
579
+ if (!fiber.dom) {
580
+ fiber.dom = createDom(fiber);
470
581
  }
582
+ const children = fiber.props?.children || [];
583
+ reconcileChildren(fiber, children);
471
584
  };
472
585
 
473
- /**
474
- * The `Image` function in JavaScript optimizes image loading based on a specified optimization flag.
475
- * @returns An `<img>` element is being returned with the specified `src` and other props passed to the
476
- * `Image` component. The `src` is either the original `src` value or the result of calling
477
- * `optimizationImageApi` function with `src` and `props` if `optimization` is set to 'true'.
478
- */
479
586
  const Image = ({ src, ...props }) => {
480
- props.optimization === 'true' ? true : false;
481
-
482
- const url = src;
483
-
484
- const ImageProps = {
485
- src: url,
486
- ...props,
487
- };
488
-
489
- return createElement('img', ImageProps, null)
587
+ return createElement('img', { ...props, src })
490
588
  };
491
589
 
492
- /**
493
- * This function uses requestIdleCallback to perform work on a fiber tree until it is complete or the
494
- * browser needs to yield to other tasks.
495
- * @param deadline - The `deadline` parameter is an object that represents the amount of time the
496
- * browser has to perform work before it needs to handle other tasks. It has a `timeRemaining()` method
497
- * that returns the amount of time remaining before the deadline is reached. The `shouldYield` variable
498
- * is used to determine
499
- */
500
590
  const workLoop = (deadline) => {
591
+ const state = getState();
501
592
  let shouldYield = false;
502
- while (vars.nextUnitOfWork && !shouldYield) {
503
- vars.nextUnitOfWork = performUnitOfWork(vars.nextUnitOfWork);
593
+ while (state.nextUnitOfWork && !shouldYield) {
594
+ state.nextUnitOfWork = performUnitOfWork(state.nextUnitOfWork);
504
595
  shouldYield = deadline.timeRemaining() < 1;
505
596
  }
506
597
 
507
- if (!vars.nextUnitOfWork && vars.wipRoot) {
598
+ if (!state.nextUnitOfWork && state.wipRoot) {
508
599
  commitRoot();
509
600
  }
510
601
 
@@ -513,18 +604,6 @@
513
604
 
514
605
  requestIdleCallback(workLoop);
515
606
 
516
- /**
517
- * The function performs a unit of work by updating either a function component or a host component and
518
- * returns the next fiber to be processed.
519
- * @param fiber - A fiber is a unit of work in Ryunix that represents a component and its state. It
520
- * contains information about the component's type, props, and children, as well as pointers to its
521
- * parent, child, and sibling fibers. The `performUnitOfWork` function takes a fiber as a parameter and
522
- * performs work
523
- * @returns The function `performUnitOfWork` returns the next fiber to be processed. If the current
524
- * fiber has a child, it returns the child. Otherwise, it looks for the next sibling of the current
525
- * fiber. If there are no more siblings, it goes up the tree to the parent and looks for the next
526
- * sibling of the parent. The function returns `null` if there are no more fibers to process.
527
- */
528
607
  const performUnitOfWork = (fiber) => {
529
608
  const isFunctionComponent = fiber.type instanceof Function;
530
609
  if (isFunctionComponent) {
@@ -545,236 +624,216 @@
545
624
  };
546
625
 
547
626
  const scheduleWork = (root) => {
548
- vars.nextUnitOfWork = root;
549
- vars.wipRoot = root;
550
- vars.deletions = [];
551
-
552
- vars.hookIndex = 0;
553
- vars.effects = [];
627
+ const state = getState();
628
+ state.nextUnitOfWork = root;
629
+ state.wipRoot = root;
630
+ state.deletions = [];
631
+ state.hookIndex = 0;
632
+ state.effects = [];
554
633
  requestIdleCallback(workLoop);
555
634
  };
556
635
 
557
- /**
558
- * Renders an element into a container using a work-in-progress (WIP) root.
559
- * @function render
560
- * @param {Object|HTMLElement} element - The element to be rendered in the container. It can be a Ryunix component (custom element) or a standard DOM element.
561
- * @param {HTMLElement} container - The container where the element will be rendered. This parameter is optional if `createRoot()` is used beforehand to set up the container.
562
- * @description The function assigns the `container` to a work-in-progress root and sets up properties for reconciliation, including children and the reference to the current root.
563
- * It also clears any scheduled deletions and establishes the next unit of work for incremental rendering.
564
- */
565
636
  const render = (element, container) => {
566
- vars.wipRoot = {
637
+ const state = getState();
638
+ state.wipRoot = {
567
639
  dom: container,
568
640
  props: {
569
641
  children: [element],
570
642
  },
571
- alternate: vars.currentRoot,
643
+ alternate: state.currentRoot,
572
644
  };
573
645
 
574
- vars.nextUnitOfWork = vars.wipRoot;
575
- vars.deletions = [];
576
- scheduleWork(vars.wipRoot);
577
- return vars.wipRoot
646
+ state.nextUnitOfWork = state.wipRoot;
647
+ state.deletions = [];
648
+ scheduleWork(state.wipRoot);
649
+ return state.wipRoot
578
650
  };
579
651
 
580
- /**
581
- * Initializes the application by creating a reference to a DOM element with the specified ID and rendering the main component.
582
- * @function init
583
- * @param {Object} MainElement - The main component to render, typically the root component of the application.
584
- * @param {string} [root='__ryunix'] - The ID of the HTML element that serves as the container for the root element. Defaults to `'__ryunix'` if not provided.
585
- * @example
586
- * Ryunix.init(App, "__ryunix"); // Initializes and renders the App component into the <div id="__ryunix"></div> element.
587
- * @description This function retrieves the container element by its ID and invokes the `render` function to render the main component into it.
588
- */
589
652
  const init = (MainElement, root = '__ryunix') => {
590
- vars.containerRoot = document.getElementById(root);
653
+ const state = getState();
654
+ state.containerRoot = document.getElementById(root);
655
+ const renderProcess = render(MainElement, state.containerRoot);
656
+ return renderProcess
657
+ };
658
+
659
+ const safeRender = (component, props, onError) => {
660
+ try {
661
+ return component(props)
662
+ } catch (error) {
663
+ if (process.env.NODE_ENV !== 'production') {
664
+ console.error('Component error:', error);
665
+ }
666
+ if (onError) onError(error);
667
+ return null
668
+ }
669
+ };
591
670
 
592
- const renderProcess = render(MainElement, vars.containerRoot);
671
+ const validateHookCall = () => {
672
+ const state = getState();
673
+ if (!state.wipFiber) {
674
+ throw new Error(
675
+ 'Hooks can only be called inside the body of a function component.',
676
+ )
677
+ }
678
+ if (!Array.isArray(state.wipFiber.hooks)) {
679
+ state.wipFiber.hooks = [];
680
+ }
681
+ };
593
682
 
594
- return renderProcess
683
+ const haveDepsChanged = (oldDeps, newDeps) => {
684
+ if (!oldDeps || !newDeps) return true
685
+ if (oldDeps.length !== newDeps.length) return true
686
+ return oldDeps.some((dep, i) => !Object.is(dep, newDeps[i]))
595
687
  };
596
688
 
597
- /**
598
- * @description The function creates a state.
599
- * @param initial - The initial value of the state for the hook.
600
- * @returns The `useStore` function returns an array with two elements: the current state value and a
601
- * `setState` function that can be used to update the state.
602
- */
603
- const useStore = (initialState, init) => {
689
+ const useStore = (initialState) => {
604
690
  const reducer = (state, action) =>
605
- typeof action === 'function' ? action(state) : action;
606
-
607
- return useReducer(reducer, initialState, init)
691
+ is.function(action) ? action(state) : action;
692
+ return useReducer(reducer, initialState)
608
693
  };
609
694
 
610
- /**
611
- * The `useReducer` function in JavaScript is used to manage state updates based on actions dispatched
612
- * to a reducer function.
613
- * @param reducer - The `reducer` parameter in the `useReducer` function is a function that takes the
614
- * current state and an action as arguments, and returns the new state based on the action. It is used
615
- * to update the state in response to different actions dispatched by the `dispatch` function.
616
- * @param initialState - The `initialState` parameter in the `useReducer` function represents the
617
- * initial state of the reducer. It is the state that will be used when the reducer is first
618
- * initialized or reset. This initial state can be any value or object that the reducer will operate on
619
- * and update based on the dispatched actions
620
- * @param init - The `init` parameter in the `useReducer` function is an optional function that can be
621
- * used to initialize the state. If provided, it will be called with the `initialState` as its argument
622
- * and the return value will be used as the initial state value. If `init` is not
623
- * @returns The `useReducer` function is returning an array with two elements: the current state and a
624
- * dispatch function.
625
- */
626
695
  const useReducer = (reducer, initialState, init) => {
627
- const oldHook =
628
- vars.wipFiber.alternate &&
629
- vars.wipFiber.alternate.hooks &&
630
- vars.wipFiber.alternate.hooks[vars.hookIndex];
696
+ validateHookCall();
697
+
698
+ const state = getState();
699
+ const { wipFiber, hookIndex } = state;
700
+ const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
631
701
 
632
702
  const hook = {
633
- hookID: vars.hookIndex,
703
+ hookID: hookIndex,
634
704
  type: RYUNIX_TYPES.RYUNIX_STORE,
635
705
  state: oldHook ? oldHook.state : init ? init(initialState) : initialState,
636
- queue: oldHook && Array.isArray(oldHook.queue) ? oldHook.queue.slice() : [],
706
+ queue: [], // Siempre nueva cola vacía
637
707
  };
638
708
 
639
- if (oldHook && Array.isArray(oldHook.queue)) {
709
+ // Procesar acciones del render anterior
710
+ if (oldHook?.queue) {
640
711
  oldHook.queue.forEach((action) => {
641
- hook.state = reducer(hook.state, action);
712
+ try {
713
+ hook.state = reducer(hook.state, action);
714
+ } catch (error) {
715
+ if (process.env.NODE_ENV !== 'production') {
716
+ console.error('Error in reducer:', error);
717
+ }
718
+ }
642
719
  });
643
720
  }
644
721
 
645
722
  const dispatch = (action) => {
646
- hook.queue.push(
647
- typeof action === STRINGS.function ? action : (prev) => action,
648
- );
723
+ if (action === undefined) {
724
+ if (process.env.NODE_ENV !== 'production') {
725
+ console.warn('dispatch called with undefined action');
726
+ }
727
+ return
728
+ }
729
+
730
+ hook.queue.push(action);
649
731
 
650
- vars.wipRoot = {
651
- dom: vars.currentRoot.dom,
652
- props: vars.currentRoot.props,
653
- alternate: vars.currentRoot,
732
+ const currentState = getState();
733
+ currentState.wipRoot = {
734
+ dom: currentState.currentRoot.dom,
735
+ props: currentState.currentRoot.props,
736
+ alternate: currentState.currentRoot,
654
737
  };
655
- vars.deletions = [];
656
- vars.hookIndex = 0;
657
- scheduleWork(vars.wipRoot);
738
+ currentState.deletions = [];
739
+ currentState.hookIndex = 0;
740
+ scheduleWork(currentState.wipRoot);
658
741
  };
659
742
 
660
- hook.queue.forEach((action) => {
661
- hook.state = reducer(hook.state, action);
662
- });
663
-
664
- vars.wipFiber.hooks[vars.hookIndex] = hook;
665
- vars.hookIndex++;
666
-
743
+ wipFiber.hooks[hookIndex] = hook;
744
+ state.hookIndex++;
667
745
  return [hook.state, dispatch]
668
746
  };
669
747
 
670
- /**
671
- * This is a function that creates a hook for managing side effects in Ryunix components.
672
- * @param effect - The effect function that will be executed after the component has rendered or when
673
- * the dependencies have changed. It can perform side effects such as fetching data, updating the DOM,
674
- * or subscribing to events.
675
- * @param deps - An array of dependencies that the effect depends on. If any of the dependencies change
676
- * between renders, the effect will be re-run. If the array is empty, the effect will only run once on
677
- * mount and never again.
678
- */
679
-
680
748
  const useEffect = (callback, deps) => {
681
- const oldHook =
682
- vars.wipFiber.alternate &&
683
- vars.wipFiber.alternate.hooks &&
684
- vars.wipFiber.alternate.hooks[vars.hookIndex];
749
+ validateHookCall();
750
+
751
+ if (!is.function(callback)) {
752
+ throw new Error('useEffect callback must be a function')
753
+ }
754
+ if (deps !== undefined && !Array.isArray(deps)) {
755
+ throw new Error('useEffect dependencies must be an array or undefined')
756
+ }
685
757
 
686
- const hasChanged = hasDepsChanged(oldHook?.deps, deps);
758
+ const state = getState();
759
+ const { wipFiber, hookIndex } = state;
760
+ const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
761
+ const hasChanged = haveDepsChanged(oldHook?.deps, deps);
687
762
 
688
763
  const hook = {
689
- hookID: vars.hookIndex,
764
+ hookID: hookIndex,
690
765
  type: RYUNIX_TYPES.RYUNIX_EFFECT,
691
766
  deps,
692
767
  effect: hasChanged ? callback : null,
693
768
  cancel: oldHook?.cancel,
694
769
  };
695
770
 
696
- vars.wipFiber.hooks[vars.hookIndex] = hook;
697
- vars.hookIndex++;
771
+ wipFiber.hooks[hookIndex] = hook;
772
+ state.hookIndex++;
698
773
  };
699
774
 
700
- /**
701
- * The useRef function in JavaScript is used to create a reference object that persists between renders
702
- * in a functional component.
703
- * @param initial - The `initial` parameter in the `useRef` function represents the initial value that
704
- * will be assigned to the `current` property of the reference object. This initial value will be used
705
- * if there is no previous value stored in the hook.
706
- * @returns The `useRef` function is returning the `current` property of the `hook.value` object. This
707
- * property contains the current value of the reference being managed by the `useRef` hook.
708
- */
709
- const useRef = (initial) => {
710
- const oldHook =
711
- vars.wipFiber.alternate &&
712
- vars.wipFiber.alternate.hooks &&
713
- vars.wipFiber.alternate.hooks[vars.hookIndex];
775
+ const useRef = (initialValue) => {
776
+ validateHookCall();
777
+
778
+ const state = getState();
779
+ const { wipFiber, hookIndex } = state;
780
+ const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
714
781
 
715
782
  const hook = {
783
+ hookID: hookIndex,
716
784
  type: RYUNIX_TYPES.RYUNIX_REF,
717
- value: oldHook ? oldHook.value : { current: initial },
785
+ value: oldHook ? oldHook.value : { current: initialValue },
718
786
  };
719
787
 
720
- vars.wipFiber.hooks[vars.hookIndex] = hook;
721
- vars.hookIndex++;
722
-
788
+ wipFiber.hooks[hookIndex] = hook;
789
+ state.hookIndex++;
723
790
  return hook.value
724
791
  };
725
792
 
726
- /**
727
- * The useMemo function in JavaScript is used to memoize the result of a computation based on the
728
- * dependencies provided.
729
- * @param comp - The `comp` parameter in the `useMemo` function is a function that represents the
730
- * computation that needs to be memoized. This function will be executed to calculate the memoized
731
- * value based on the dependencies provided.
732
- * @param deps - The `deps` parameter in the `useMemo` function stands for dependencies. It is an array
733
- * of values that the function depends on. The `useMemo` function will only recompute the memoized
734
- * value when one of the dependencies has changed.
735
- * @returns The `useMemo` function returns the `value` property of the `hook` object, which is either
736
- * the memoized value from the previous render if the dependencies have not changed, or the result of
737
- * calling the `comp` function if the dependencies have changed.
738
- */
739
- const useMemo = (comp, deps) => {
740
- const oldHook =
741
- vars.wipFiber.alternate &&
742
- vars.wipFiber.alternate.hooks &&
743
- vars.wipFiber.alternate.hooks[vars.hookIndex];
793
+ const useMemo = (compute, deps) => {
794
+ validateHookCall();
744
795
 
745
- const hook = {
746
- type: RYUNIX_TYPES.RYUNIX_MEMO,
747
- value: null,
748
- deps,
749
- };
796
+ if (!is.function(compute)) {
797
+ throw new Error('useMemo callback must be a function')
798
+ }
799
+ if (!Array.isArray(deps)) {
800
+ throw new Error('useMemo requires a dependencies array')
801
+ }
750
802
 
751
- if (oldHook) {
752
- if (lodash.isEqual(oldHook.deps, hook.deps)) {
753
- hook.value = oldHook.value;
754
- } else {
755
- hook.value = comp();
756
- }
803
+ const state = getState();
804
+ const { wipFiber, hookIndex } = state;
805
+ const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
806
+
807
+ let value;
808
+ if (oldHook && !haveDepsChanged(oldHook.deps, deps)) {
809
+ value = oldHook.value;
757
810
  } else {
758
- hook.value = comp();
811
+ try {
812
+ value = compute();
813
+ } catch (error) {
814
+ if (process.env.NODE_ENV !== 'production') {
815
+ console.error('Error in useMemo computation:', error);
816
+ }
817
+ value = undefined;
818
+ }
759
819
  }
760
820
 
761
- vars.wipFiber.hooks[vars.hookIndex] = hook;
762
- vars.hookIndex++;
821
+ const hook = {
822
+ hookID: hookIndex,
823
+ type: RYUNIX_TYPES.RYUNIX_MEMO,
824
+ value,
825
+ deps,
826
+ };
763
827
 
764
- return hook.value
828
+ wipFiber.hooks[hookIndex] = hook;
829
+ state.hookIndex++;
830
+ return value
765
831
  };
766
832
 
767
- /**
768
- * The useCallback function in JavaScript returns a memoized version of the callback function that only
769
- * changes if one of the dependencies has changed.
770
- * @param callback - The `callback` parameter is a function that you want to memoize using
771
- * `useCallback`. This function will only be re-created if any of the dependencies specified in the
772
- * `deps` array change.
773
- * @param deps - Dependencies array that the callback function depends on.
774
- * @returns The useCallback function is returning a memoized version of the callback function. It is
775
- * using the useMemo hook to memoize the callback function based on the provided dependencies (deps).
776
- */
777
833
  const useCallback = (callback, deps) => {
834
+ if (!is.function(callback)) {
835
+ throw new Error('useCallback requires a function as first argument')
836
+ }
778
837
  return useMemo(() => callback, deps)
779
838
  };
780
839
 
@@ -782,55 +841,109 @@
782
841
  contextId = RYUNIX_TYPES.RYUNIX_CONTEXT,
783
842
  defaultValue = {},
784
843
  ) => {
785
- const Provider = ({ children }) => {
786
- return Fragment({
787
- children: children,
788
- })
844
+ const Provider = ({ children, value }) => {
845
+ const element = Fragment({ children });
846
+ element._contextId = contextId;
847
+ element._contextValue = value;
848
+ return element
789
849
  };
790
850
 
791
851
  Provider._contextId = contextId;
792
852
 
793
- const useContext = (ctxID = RYUNIX_TYPES.RYUNIX_CONTEXT) => {
794
- let fiber = vars.wipFiber;
853
+ const useContext = (ctxID = contextId) => {
854
+ validateHookCall();
855
+
856
+ const state = getState();
857
+ let fiber = state.wipFiber;
858
+
795
859
  while (fiber) {
796
- if (fiber.type && fiber.type._contextId === ctxID) {
797
- if (fiber.props && 'value' in fiber.props) {
798
- return fiber.props.value
799
- }
800
- return undefined
860
+ if (fiber._contextId === ctxID && fiber._contextValue !== undefined) {
861
+ return fiber._contextValue
862
+ }
863
+ if (
864
+ fiber.type?._contextId === ctxID &&
865
+ fiber.props?.value !== undefined
866
+ ) {
867
+ return fiber.props.value
801
868
  }
802
869
  fiber = fiber.parent;
803
870
  }
804
871
  return defaultValue
805
872
  };
806
873
 
807
- return {
808
- Provider,
809
- useContext,
810
- }
874
+ return { Provider, useContext }
811
875
  };
812
876
 
813
877
  const useQuery = () => {
878
+ if (typeof window === 'undefined') return {}
879
+
814
880
  const searchParams = new URLSearchParams(window.location.search);
815
881
  const query = {};
816
- for (let [key, value] of searchParams.entries()) {
882
+ for (const [key, value] of searchParams.entries()) {
817
883
  query[key] = value;
818
884
  }
819
885
  return query
820
886
  };
821
887
 
822
888
  const useHash = () => {
889
+ if (typeof window === 'undefined') return ''
890
+
823
891
  const [hash, setHash] = useStore(window.location.hash);
824
892
  useEffect(() => {
825
- const onHashChange = () => {
826
- setHash(window.location.hash);
827
- };
893
+ const onHashChange = () => setHash(window.location.hash);
828
894
  window.addEventListener('hashchange', onHashChange);
829
895
  return () => window.removeEventListener('hashchange', onHashChange)
830
896
  }, []);
831
897
  return hash
832
898
  };
833
899
 
900
+ const useMetadata = (tags = {}, options = {}) => {
901
+ useEffect(() => {
902
+ if (typeof document === 'undefined') return
903
+
904
+ let finalTitle = 'Ryunix App';
905
+ const template = options.title?.template;
906
+ const defaultTitle = options.title?.prefix || 'Ryunix App';
907
+ const pageTitle = tags.pageTitle || tags.title;
908
+
909
+ if (is.string(pageTitle) && pageTitle.trim()) {
910
+ finalTitle = template?.includes('%s')
911
+ ? template.replace('%s', pageTitle)
912
+ : pageTitle;
913
+ } else {
914
+ finalTitle = defaultTitle;
915
+ }
916
+
917
+ document.title = finalTitle;
918
+
919
+ if (tags.canonical) {
920
+ let link = document.querySelector('link[rel="canonical"]');
921
+ if (!link) {
922
+ link = document.createElement('link');
923
+ link.setAttribute('rel', 'canonical');
924
+ document.head.appendChild(link);
925
+ }
926
+ link.setAttribute('href', tags.canonical);
927
+ }
928
+
929
+ Object.entries(tags).forEach(([key, value]) => {
930
+ if (['title', 'pageTitle', 'canonical'].includes(key)) return
931
+
932
+ const isProperty = key.startsWith('og:') || key.startsWith('twitter:');
933
+ const selector = `meta[${isProperty ? 'property' : 'name'}='${key}']`;
934
+ let meta = document.head.querySelector(selector);
935
+
936
+ if (!meta) {
937
+ meta = document.createElement('meta');
938
+ meta.setAttribute(isProperty ? 'property' : 'name', key);
939
+ document.head.appendChild(meta);
940
+ }
941
+ meta.setAttribute('content', value);
942
+ });
943
+ }, [JSON.stringify(tags), JSON.stringify(options)]);
944
+ };
945
+
946
+ // Router Context
834
947
  const RouterContext = createContext('ryunix.navigation', {
835
948
  location: '/',
836
949
  params: {},
@@ -841,7 +954,6 @@
841
954
 
842
955
  const findRoute = (routes, path) => {
843
956
  const pathname = path.split('?')[0].split('#')[0];
844
-
845
957
  const notFoundRoute = routes.find((route) => route.NotFound);
846
958
  const notFound = notFoundRoute
847
959
  ? { route: { component: notFoundRoute.NotFound }, params: {} }
@@ -852,18 +964,8 @@
852
964
  const childRoute = findRoute(route.subRoutes, path);
853
965
  if (childRoute) return childRoute
854
966
  }
855
-
856
- if (route.path === '*') {
857
- return notFound
858
- }
859
-
860
- if (!route.path || typeof route.path !== 'string') {
861
- console.warn('Invalid route detected:', route);
862
- console.info(
863
- "if you are using { NotFound: NotFound } please add { path: '*', NotFound: NotFound }",
864
- );
865
- continue
866
- }
967
+ if (route.path === '*') return notFound
968
+ if (!route.path || typeof route.path !== 'string') continue
867
969
 
868
970
  const keys = [];
869
971
  const pattern = new RegExp(
@@ -879,11 +981,9 @@
879
981
  acc[key] = match[index + 1];
880
982
  return acc
881
983
  }, {});
882
-
883
984
  return { route, params }
884
985
  }
885
986
  }
886
-
887
987
  return notFound
888
988
  };
889
989
 
@@ -892,7 +992,6 @@
892
992
 
893
993
  useEffect(() => {
894
994
  const update = () => setLocation(window.location.pathname);
895
-
896
995
  window.addEventListener('popstate', update);
897
996
  window.addEventListener('hashchange', update);
898
997
  return () => {
@@ -907,7 +1006,6 @@
907
1006
  };
908
1007
 
909
1008
  const currentRouteData = findRoute(routes, location) || {};
910
-
911
1009
  const query = useQuery();
912
1010
 
913
1011
  const contextValue = {
@@ -921,9 +1019,7 @@
921
1019
  return createElement(
922
1020
  RouterContext.Provider,
923
1021
  { value: contextValue },
924
- Fragment({
925
- children: children,
926
- }),
1022
+ Fragment({ children }),
927
1023
  )
928
1024
  };
929
1025
 
@@ -954,7 +1050,6 @@
954
1050
 
955
1051
  const NavLink = ({ to, exact = false, ...props }) => {
956
1052
  const { location, navigate } = useRouter();
957
-
958
1053
  const isActive = exact ? location === to : location.startsWith(to);
959
1054
 
960
1055
  const resolveClass = (cls) =>
@@ -966,7 +1061,6 @@
966
1061
  };
967
1062
 
968
1063
  const classAttrName = props['ryunix-class'] ? 'ryunix-class' : 'className';
969
-
970
1064
  const classAttrValue = resolveClass(
971
1065
  props['ryunix-class'] || props['className'],
972
1066
  );
@@ -989,77 +1083,6 @@
989
1083
  )
990
1084
  };
991
1085
 
992
- /**
993
- * useMetadata: Hook to dynamically manage SEO metadata in the <head>.
994
- * Supports title with template, description, robots, robots, canonical, OpenGraph, Twitter, and any standard meta.
995
- * @param {Object} tags - Object with metatags to insert/update.
996
- * @param {Object} options - Optional. Allows to define template and default for the title.
997
-
998
- */
999
-
1000
- const useMetadata = (tags = {}, options = {}) => {
1001
- useEffect(() => {
1002
- if (typeof document === 'undefined') return // SSR safe
1003
-
1004
- let finalTitle = '';
1005
- let template = undefined;
1006
- let defaultTitle = 'Ryunix App';
1007
- if (options.title && typeof options.title === 'object') {
1008
- template = options.title.template;
1009
- if (typeof options.title.prefix === 'string') {
1010
- defaultTitle = options.title.prefix;
1011
- }
1012
- }
1013
-
1014
- // pageTitle tiene prioridad sobre title
1015
- let pageTitle = tags.pageTitle || tags.title;
1016
-
1017
- if (typeof pageTitle === 'string') {
1018
- if (pageTitle.trim() === '') {
1019
- finalTitle = defaultTitle;
1020
- } else if (template && template.includes('%s')) {
1021
- finalTitle = template.replace('%s', pageTitle);
1022
- } else {
1023
- finalTitle = pageTitle;
1024
- }
1025
- } else if (typeof pageTitle === 'object' && pageTitle !== null) {
1026
- finalTitle = defaultTitle;
1027
- } else if (!pageTitle) {
1028
- finalTitle = defaultTitle;
1029
- }
1030
- document.title = finalTitle;
1031
- // Canonical
1032
- if (tags.canonical) {
1033
- let link = document.querySelector('link[rel="canonical"]');
1034
- if (!link) {
1035
- link = document.createElement('link');
1036
- link.setAttribute('rel', 'canonical');
1037
- document.head.appendChild(link);
1038
- }
1039
- link.setAttribute('href', tags.canonical);
1040
- }
1041
- // Meta tags
1042
- Object.entries(tags).forEach(([key, value]) => {
1043
- if (key === 'title' || key === 'pageTitle' || key === 'canonical') return
1044
- let selector = `meta[name='${key}']`;
1045
- if (key.startsWith('og:') || key.startsWith('twitter:')) {
1046
- selector = `meta[property='${key}']`;
1047
- }
1048
- let meta = document.head.querySelector(selector);
1049
- if (!meta) {
1050
- meta = document.createElement('meta');
1051
- if (key.startsWith('og:') || key.startsWith('twitter:')) {
1052
- meta.setAttribute('property', key);
1053
- } else {
1054
- meta.setAttribute('name', key);
1055
- }
1056
- document.head.appendChild(meta);
1057
- }
1058
- meta.setAttribute('content', value);
1059
- });
1060
- }, [JSON.stringify(tags), JSON.stringify(options)]);
1061
- };
1062
-
1063
1086
  var Hooks = /*#__PURE__*/Object.freeze({
1064
1087
  __proto__: null,
1065
1088
  Children: Children,
@@ -1072,17 +1095,126 @@
1072
1095
  useMemo: useMemo,
1073
1096
  useMetadata: useMetadata,
1074
1097
  useQuery: useQuery,
1098
+ useReducer: useReducer,
1075
1099
  useRef: useRef,
1076
1100
  useRouter: useRouter,
1077
1101
  useStore: useStore
1078
1102
  });
1079
1103
 
1104
+ /**
1105
+ * memo - Memoize component to prevent unnecessary re-renders
1106
+ */
1107
+ const memo = (Component, arePropsEqual) => {
1108
+ return (props) => {
1109
+ const memoizedElement = useMemo(() => {
1110
+ return Component(props)
1111
+ }, [
1112
+ // Default comparison: shallow props comparison
1113
+ ...Object.values(props),
1114
+ ]);
1115
+
1116
+ return memoizedElement
1117
+ }
1118
+ };
1119
+
1120
+ /**
1121
+ * Lazy load component
1122
+ */
1123
+ const lazy = (importFn) => {
1124
+ let Component = null;
1125
+ let promise = null;
1126
+ let error = null;
1127
+
1128
+ return (props) => {
1129
+ const [, forceUpdate] = useStore(0);
1130
+
1131
+ useEffect(() => {
1132
+ if (Component || error) return
1133
+
1134
+ if (!promise) {
1135
+ promise = importFn()
1136
+ .then((module) => {
1137
+ Component = module.default || module;
1138
+ forceUpdate((x) => x + 1);
1139
+ })
1140
+ .catch((err) => {
1141
+ error = err;
1142
+ forceUpdate((x) => x + 1);
1143
+ });
1144
+ }
1145
+ }, []);
1146
+
1147
+ if (error) throw error
1148
+ if (!Component) return null
1149
+ return createElement(Component, props)
1150
+ }
1151
+ };
1152
+
1153
+ /**
1154
+ * Suspense component (basic implementation)
1155
+ */
1156
+ const Suspense = ({ fallback, children }) => {
1157
+ const [isLoading, setIsLoading] = useStore(true);
1158
+
1159
+ useEffect(() => {
1160
+ setIsLoading(false);
1161
+ }, []);
1162
+
1163
+ if (isLoading && fallback) {
1164
+ return fallback
1165
+ }
1166
+
1167
+ return children
1168
+ };
1169
+
1170
+ let isBatching = false;
1171
+ let pendingUpdates = [];
1172
+
1173
+ /**
1174
+ * Batch multiple state updates into single render
1175
+ */
1176
+ const batchUpdates = (callback) => {
1177
+ const wasBatching = isBatching;
1178
+ isBatching = true;
1179
+
1180
+ try {
1181
+ callback();
1182
+ } finally {
1183
+ isBatching = wasBatching;
1184
+
1185
+ if (!isBatching && pendingUpdates.length > 0) {
1186
+ flushUpdates();
1187
+ }
1188
+ }
1189
+ };
1190
+
1191
+ /**
1192
+ * Flush all pending updates
1193
+ */
1194
+ const flushUpdates = () => {
1195
+ if (pendingUpdates.length === 0) return
1196
+
1197
+ const updates = pendingUpdates;
1198
+ pendingUpdates = [];
1199
+
1200
+ // Execute all updates
1201
+ updates.forEach((update) => update());
1202
+ };
1203
+
1204
+ // Ryunix.*
1080
1205
  var Ryunix = {
1081
1206
  createElement,
1082
1207
  render,
1083
1208
  init,
1084
1209
  Fragment,
1085
1210
  Hooks,
1211
+
1212
+ memo,
1213
+ lazy,
1214
+ Suspense,
1215
+
1216
+ safeRender,
1217
+ batchUpdates,
1086
1218
  };
1087
1219
 
1088
1220
  window.Ryunix = Ryunix;
@@ -1099,6 +1231,7 @@
1099
1231
  exports.useMemo = useMemo;
1100
1232
  exports.useMetadata = useMetadata;
1101
1233
  exports.useQuery = useQuery;
1234
+ exports.useReducer = useReducer;
1102
1235
  exports.useRef = useRef;
1103
1236
  exports.useRouter = useRouter;
1104
1237
  exports.useStore = useStore;