@unsetsoft/ryunixjs 1.2.1 → 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;
196
248
  }
197
249
  }
198
250
  };
199
251
 
200
252
  /**
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
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')
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);
316
+ }
317
+ };
318
+
319
+ /**
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,108 +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
- /* Internal components*/
474
-
475
- /**
476
- * The function `optimizationImageApi` optimizes image URLs by adding query parameters for width,
477
- * height, quality, and extension, and handles local and remote image sources.
478
- * @returns The function `optimizationImageApi` returns either the original `src` if it is a local
479
- * image and the page is being run on localhost, or it returns a modified image URL with optimization
480
- * parameters added if the `src` is not local.
481
- */
482
-
483
- const isLocalhost = () => {
484
- const { hostname } = window.location;
485
- return hostname === 'localhost' || hostname === '127.0.0.1'
486
- };
487
-
488
- const optimizationImageApi = ({ src, props }) => {
489
- const query = new URLSearchParams();
490
- const apiEndpoint = 'https://image.unsetsoft.com';
491
-
492
- const isLocal = !src.startsWith('http') || !src.startsWith('https');
493
-
494
- if (props.width) query.set('width', props.width);
495
- if (props.height) query.set('height', props.height);
496
- if (props.quality) query.set('quality', props.quality);
497
-
498
- const extension = props.extension ? `@${props.extension}` : '';
499
-
500
- if (isLocal) {
501
- if (isLocalhost()) {
502
- console.warn(
503
- 'Image optimizations only work with full links and must not contain localhost.',
504
- );
505
- return src
506
- }
507
-
508
- return `${window.location.origin}/${src}`
509
- }
510
-
511
- return `${apiEndpoint}/image/${src}${extension}?${query.toString()}`
512
- };
513
-
514
- /**
515
- * The `Image` function in JavaScript optimizes image loading based on a specified optimization flag.
516
- * @returns An `<img>` element is being returned with the specified `src` and other props passed to the
517
- * `Image` component. The `src` is either the original `src` value or the result of calling
518
- * `optimizationImageApi` function with `src` and `props` if `optimization` is set to 'true'.
519
- */
520
586
  const Image = ({ src, ...props }) => {
521
- const optimization = props.optimization === 'true' ? true : false;
522
-
523
- const url = optimization
524
- ? optimizationImageApi({
525
- src,
526
- props,
527
- })
528
- : src;
529
-
530
- const ImageProps = {
531
- src: url,
532
- ...props,
533
- };
534
-
535
- return createElement('img', ImageProps, null)
587
+ return createElement('img', { ...props, src })
536
588
  };
537
589
 
538
- /**
539
- * This function uses requestIdleCallback to perform work on a fiber tree until it is complete or the
540
- * browser needs to yield to other tasks.
541
- * @param deadline - The `deadline` parameter is an object that represents the amount of time the
542
- * browser has to perform work before it needs to handle other tasks. It has a `timeRemaining()` method
543
- * that returns the amount of time remaining before the deadline is reached. The `shouldYield` variable
544
- * is used to determine
545
- */
546
590
  const workLoop = (deadline) => {
591
+ const state = getState();
547
592
  let shouldYield = false;
548
- while (vars.nextUnitOfWork && !shouldYield) {
549
- vars.nextUnitOfWork = performUnitOfWork(vars.nextUnitOfWork);
593
+ while (state.nextUnitOfWork && !shouldYield) {
594
+ state.nextUnitOfWork = performUnitOfWork(state.nextUnitOfWork);
550
595
  shouldYield = deadline.timeRemaining() < 1;
551
596
  }
552
597
 
553
- if (!vars.nextUnitOfWork && vars.wipRoot) {
598
+ if (!state.nextUnitOfWork && state.wipRoot) {
554
599
  commitRoot();
555
600
  }
556
601
 
@@ -559,18 +604,6 @@
559
604
 
560
605
  requestIdleCallback(workLoop);
561
606
 
562
- /**
563
- * The function performs a unit of work by updating either a function component or a host component and
564
- * returns the next fiber to be processed.
565
- * @param fiber - A fiber is a unit of work in Ryunix that represents a component and its state. It
566
- * contains information about the component's type, props, and children, as well as pointers to its
567
- * parent, child, and sibling fibers. The `performUnitOfWork` function takes a fiber as a parameter and
568
- * performs work
569
- * @returns The function `performUnitOfWork` returns the next fiber to be processed. If the current
570
- * fiber has a child, it returns the child. Otherwise, it looks for the next sibling of the current
571
- * fiber. If there are no more siblings, it goes up the tree to the parent and looks for the next
572
- * sibling of the parent. The function returns `null` if there are no more fibers to process.
573
- */
574
607
  const performUnitOfWork = (fiber) => {
575
608
  const isFunctionComponent = fiber.type instanceof Function;
576
609
  if (isFunctionComponent) {
@@ -591,236 +624,216 @@
591
624
  };
592
625
 
593
626
  const scheduleWork = (root) => {
594
- vars.nextUnitOfWork = root;
595
- vars.wipRoot = root;
596
- vars.deletions = [];
597
-
598
- vars.hookIndex = 0;
599
- vars.effects = [];
627
+ const state = getState();
628
+ state.nextUnitOfWork = root;
629
+ state.wipRoot = root;
630
+ state.deletions = [];
631
+ state.hookIndex = 0;
632
+ state.effects = [];
600
633
  requestIdleCallback(workLoop);
601
634
  };
602
635
 
603
- /**
604
- * Renders an element into a container using a work-in-progress (WIP) root.
605
- * @function render
606
- * @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.
607
- * @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.
608
- * @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.
609
- * It also clears any scheduled deletions and establishes the next unit of work for incremental rendering.
610
- */
611
636
  const render = (element, container) => {
612
- vars.wipRoot = {
637
+ const state = getState();
638
+ state.wipRoot = {
613
639
  dom: container,
614
640
  props: {
615
641
  children: [element],
616
642
  },
617
- alternate: vars.currentRoot,
643
+ alternate: state.currentRoot,
618
644
  };
619
645
 
620
- vars.nextUnitOfWork = vars.wipRoot;
621
- vars.deletions = [];
622
- scheduleWork(vars.wipRoot);
623
- return vars.wipRoot
646
+ state.nextUnitOfWork = state.wipRoot;
647
+ state.deletions = [];
648
+ scheduleWork(state.wipRoot);
649
+ return state.wipRoot
624
650
  };
625
651
 
626
- /**
627
- * Initializes the application by creating a reference to a DOM element with the specified ID and rendering the main component.
628
- * @function init
629
- * @param {Object} MainElement - The main component to render, typically the root component of the application.
630
- * @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.
631
- * @example
632
- * Ryunix.init(App, "__ryunix"); // Initializes and renders the App component into the <div id="__ryunix"></div> element.
633
- * @description This function retrieves the container element by its ID and invokes the `render` function to render the main component into it.
634
- */
635
652
  const init = (MainElement, root = '__ryunix') => {
636
- 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
+ };
637
658
 
638
- const renderProcess = render(MainElement, vars.containerRoot);
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
+ };
639
670
 
640
- return renderProcess
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
+ }
641
681
  };
642
682
 
643
- /**
644
- * @description The function creates a state.
645
- * @param initial - The initial value of the state for the hook.
646
- * @returns The `useStore` function returns an array with two elements: the current state value and a
647
- * `setState` function that can be used to update the state.
648
- */
649
- const useStore = (initialState, init) => {
650
- const reducer = (state, action) =>
651
- typeof action === 'function' ? action(state) : action;
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]))
687
+ };
652
688
 
653
- return useReducer(reducer, initialState, init)
689
+ const useStore = (initialState) => {
690
+ const reducer = (state, action) =>
691
+ is.function(action) ? action(state) : action;
692
+ return useReducer(reducer, initialState)
654
693
  };
655
694
 
656
- /**
657
- * The `useReducer` function in JavaScript is used to manage state updates based on actions dispatched
658
- * to a reducer function.
659
- * @param reducer - The `reducer` parameter in the `useReducer` function is a function that takes the
660
- * current state and an action as arguments, and returns the new state based on the action. It is used
661
- * to update the state in response to different actions dispatched by the `dispatch` function.
662
- * @param initialState - The `initialState` parameter in the `useReducer` function represents the
663
- * initial state of the reducer. It is the state that will be used when the reducer is first
664
- * initialized or reset. This initial state can be any value or object that the reducer will operate on
665
- * and update based on the dispatched actions
666
- * @param init - The `init` parameter in the `useReducer` function is an optional function that can be
667
- * used to initialize the state. If provided, it will be called with the `initialState` as its argument
668
- * and the return value will be used as the initial state value. If `init` is not
669
- * @returns The `useReducer` function is returning an array with two elements: the current state and a
670
- * dispatch function.
671
- */
672
695
  const useReducer = (reducer, initialState, init) => {
673
- const oldHook =
674
- vars.wipFiber.alternate &&
675
- vars.wipFiber.alternate.hooks &&
676
- 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];
677
701
 
678
702
  const hook = {
679
- hookID: vars.hookIndex,
703
+ hookID: hookIndex,
680
704
  type: RYUNIX_TYPES.RYUNIX_STORE,
681
705
  state: oldHook ? oldHook.state : init ? init(initialState) : initialState,
682
- queue: oldHook && Array.isArray(oldHook.queue) ? oldHook.queue.slice() : [],
706
+ queue: [], // Siempre nueva cola vacía
683
707
  };
684
708
 
685
- if (oldHook && Array.isArray(oldHook.queue)) {
709
+ // Procesar acciones del render anterior
710
+ if (oldHook?.queue) {
686
711
  oldHook.queue.forEach((action) => {
687
- 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
+ }
688
719
  });
689
720
  }
690
721
 
691
722
  const dispatch = (action) => {
692
- hook.queue.push(
693
- typeof action === STRINGS.function ? action : (prev) => action,
694
- );
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);
695
731
 
696
- vars.wipRoot = {
697
- dom: vars.currentRoot.dom,
698
- props: vars.currentRoot.props,
699
- alternate: vars.currentRoot,
732
+ const currentState = getState();
733
+ currentState.wipRoot = {
734
+ dom: currentState.currentRoot.dom,
735
+ props: currentState.currentRoot.props,
736
+ alternate: currentState.currentRoot,
700
737
  };
701
- vars.deletions = [];
702
- vars.hookIndex = 0;
703
- scheduleWork(vars.wipRoot);
738
+ currentState.deletions = [];
739
+ currentState.hookIndex = 0;
740
+ scheduleWork(currentState.wipRoot);
704
741
  };
705
742
 
706
- hook.queue.forEach((action) => {
707
- hook.state = reducer(hook.state, action);
708
- });
709
-
710
- vars.wipFiber.hooks[vars.hookIndex] = hook;
711
- vars.hookIndex++;
712
-
743
+ wipFiber.hooks[hookIndex] = hook;
744
+ state.hookIndex++;
713
745
  return [hook.state, dispatch]
714
746
  };
715
747
 
716
- /**
717
- * This is a function that creates a hook for managing side effects in Ryunix components.
718
- * @param effect - The effect function that will be executed after the component has rendered or when
719
- * the dependencies have changed. It can perform side effects such as fetching data, updating the DOM,
720
- * or subscribing to events.
721
- * @param deps - An array of dependencies that the effect depends on. If any of the dependencies change
722
- * between renders, the effect will be re-run. If the array is empty, the effect will only run once on
723
- * mount and never again.
724
- */
725
-
726
748
  const useEffect = (callback, deps) => {
727
- const oldHook =
728
- vars.wipFiber.alternate &&
729
- vars.wipFiber.alternate.hooks &&
730
- 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
+ }
731
757
 
732
- 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);
733
762
 
734
763
  const hook = {
735
- hookID: vars.hookIndex,
764
+ hookID: hookIndex,
736
765
  type: RYUNIX_TYPES.RYUNIX_EFFECT,
737
766
  deps,
738
767
  effect: hasChanged ? callback : null,
739
768
  cancel: oldHook?.cancel,
740
769
  };
741
770
 
742
- vars.wipFiber.hooks[vars.hookIndex] = hook;
743
- vars.hookIndex++;
771
+ wipFiber.hooks[hookIndex] = hook;
772
+ state.hookIndex++;
744
773
  };
745
774
 
746
- /**
747
- * The useRef function in JavaScript is used to create a reference object that persists between renders
748
- * in a functional component.
749
- * @param initial - The `initial` parameter in the `useRef` function represents the initial value that
750
- * will be assigned to the `current` property of the reference object. This initial value will be used
751
- * if there is no previous value stored in the hook.
752
- * @returns The `useRef` function is returning the `current` property of the `hook.value` object. This
753
- * property contains the current value of the reference being managed by the `useRef` hook.
754
- */
755
- const useRef = (initial) => {
756
- const oldHook =
757
- vars.wipFiber.alternate &&
758
- vars.wipFiber.alternate.hooks &&
759
- 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];
760
781
 
761
782
  const hook = {
783
+ hookID: hookIndex,
762
784
  type: RYUNIX_TYPES.RYUNIX_REF,
763
- value: oldHook ? oldHook.value : { current: initial },
785
+ value: oldHook ? oldHook.value : { current: initialValue },
764
786
  };
765
787
 
766
- vars.wipFiber.hooks[vars.hookIndex] = hook;
767
- vars.hookIndex++;
768
-
788
+ wipFiber.hooks[hookIndex] = hook;
789
+ state.hookIndex++;
769
790
  return hook.value
770
791
  };
771
792
 
772
- /**
773
- * The useMemo function in JavaScript is used to memoize the result of a computation based on the
774
- * dependencies provided.
775
- * @param comp - The `comp` parameter in the `useMemo` function is a function that represents the
776
- * computation that needs to be memoized. This function will be executed to calculate the memoized
777
- * value based on the dependencies provided.
778
- * @param deps - The `deps` parameter in the `useMemo` function stands for dependencies. It is an array
779
- * of values that the function depends on. The `useMemo` function will only recompute the memoized
780
- * value when one of the dependencies has changed.
781
- * @returns The `useMemo` function returns the `value` property of the `hook` object, which is either
782
- * the memoized value from the previous render if the dependencies have not changed, or the result of
783
- * calling the `comp` function if the dependencies have changed.
784
- */
785
- const useMemo = (comp, deps) => {
786
- const oldHook =
787
- vars.wipFiber.alternate &&
788
- vars.wipFiber.alternate.hooks &&
789
- vars.wipFiber.alternate.hooks[vars.hookIndex];
793
+ const useMemo = (compute, deps) => {
794
+ validateHookCall();
790
795
 
791
- const hook = {
792
- type: RYUNIX_TYPES.RYUNIX_MEMO,
793
- value: null,
794
- deps,
795
- };
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
+ }
796
802
 
797
- if (oldHook) {
798
- if (lodash.isEqual(oldHook.deps, hook.deps)) {
799
- hook.value = oldHook.value;
800
- } else {
801
- hook.value = comp();
802
- }
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;
803
810
  } else {
804
- 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
+ }
805
819
  }
806
820
 
807
- vars.wipFiber.hooks[vars.hookIndex] = hook;
808
- vars.hookIndex++;
821
+ const hook = {
822
+ hookID: hookIndex,
823
+ type: RYUNIX_TYPES.RYUNIX_MEMO,
824
+ value,
825
+ deps,
826
+ };
809
827
 
810
- return hook.value
828
+ wipFiber.hooks[hookIndex] = hook;
829
+ state.hookIndex++;
830
+ return value
811
831
  };
812
832
 
813
- /**
814
- * The useCallback function in JavaScript returns a memoized version of the callback function that only
815
- * changes if one of the dependencies has changed.
816
- * @param callback - The `callback` parameter is a function that you want to memoize using
817
- * `useCallback`. This function will only be re-created if any of the dependencies specified in the
818
- * `deps` array change.
819
- * @param deps - Dependencies array that the callback function depends on.
820
- * @returns The useCallback function is returning a memoized version of the callback function. It is
821
- * using the useMemo hook to memoize the callback function based on the provided dependencies (deps).
822
- */
823
833
  const useCallback = (callback, deps) => {
834
+ if (!is.function(callback)) {
835
+ throw new Error('useCallback requires a function as first argument')
836
+ }
824
837
  return useMemo(() => callback, deps)
825
838
  };
826
839
 
@@ -828,55 +841,109 @@
828
841
  contextId = RYUNIX_TYPES.RYUNIX_CONTEXT,
829
842
  defaultValue = {},
830
843
  ) => {
831
- const Provider = ({ children }) => {
832
- return Fragment({
833
- children: children,
834
- })
844
+ const Provider = ({ children, value }) => {
845
+ const element = Fragment({ children });
846
+ element._contextId = contextId;
847
+ element._contextValue = value;
848
+ return element
835
849
  };
836
850
 
837
851
  Provider._contextId = contextId;
838
852
 
839
- const useContext = (ctxID = RYUNIX_TYPES.RYUNIX_CONTEXT) => {
840
- let fiber = vars.wipFiber;
853
+ const useContext = (ctxID = contextId) => {
854
+ validateHookCall();
855
+
856
+ const state = getState();
857
+ let fiber = state.wipFiber;
858
+
841
859
  while (fiber) {
842
- if (fiber.type && fiber.type._contextId === ctxID) {
843
- if (fiber.props && 'value' in fiber.props) {
844
- return fiber.props.value
845
- }
846
- 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
847
868
  }
848
869
  fiber = fiber.parent;
849
870
  }
850
871
  return defaultValue
851
872
  };
852
873
 
853
- return {
854
- Provider,
855
- useContext,
856
- }
874
+ return { Provider, useContext }
857
875
  };
858
876
 
859
877
  const useQuery = () => {
878
+ if (typeof window === 'undefined') return {}
879
+
860
880
  const searchParams = new URLSearchParams(window.location.search);
861
881
  const query = {};
862
- for (let [key, value] of searchParams.entries()) {
882
+ for (const [key, value] of searchParams.entries()) {
863
883
  query[key] = value;
864
884
  }
865
885
  return query
866
886
  };
867
887
 
868
888
  const useHash = () => {
889
+ if (typeof window === 'undefined') return ''
890
+
869
891
  const [hash, setHash] = useStore(window.location.hash);
870
892
  useEffect(() => {
871
- const onHashChange = () => {
872
- setHash(window.location.hash);
873
- };
893
+ const onHashChange = () => setHash(window.location.hash);
874
894
  window.addEventListener('hashchange', onHashChange);
875
895
  return () => window.removeEventListener('hashchange', onHashChange)
876
896
  }, []);
877
897
  return hash
878
898
  };
879
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
880
947
  const RouterContext = createContext('ryunix.navigation', {
881
948
  location: '/',
882
949
  params: {},
@@ -887,7 +954,6 @@
887
954
 
888
955
  const findRoute = (routes, path) => {
889
956
  const pathname = path.split('?')[0].split('#')[0];
890
-
891
957
  const notFoundRoute = routes.find((route) => route.NotFound);
892
958
  const notFound = notFoundRoute
893
959
  ? { route: { component: notFoundRoute.NotFound }, params: {} }
@@ -898,18 +964,8 @@
898
964
  const childRoute = findRoute(route.subRoutes, path);
899
965
  if (childRoute) return childRoute
900
966
  }
901
-
902
- if (route.path === '*') {
903
- return notFound
904
- }
905
-
906
- if (!route.path || typeof route.path !== 'string') {
907
- console.warn('Invalid route detected:', route);
908
- console.info(
909
- "if you are using { NotFound: NotFound } please add { path: '*', NotFound: NotFound }",
910
- );
911
- continue
912
- }
967
+ if (route.path === '*') return notFound
968
+ if (!route.path || typeof route.path !== 'string') continue
913
969
 
914
970
  const keys = [];
915
971
  const pattern = new RegExp(
@@ -925,11 +981,9 @@
925
981
  acc[key] = match[index + 1];
926
982
  return acc
927
983
  }, {});
928
-
929
984
  return { route, params }
930
985
  }
931
986
  }
932
-
933
987
  return notFound
934
988
  };
935
989
 
@@ -938,7 +992,6 @@
938
992
 
939
993
  useEffect(() => {
940
994
  const update = () => setLocation(window.location.pathname);
941
-
942
995
  window.addEventListener('popstate', update);
943
996
  window.addEventListener('hashchange', update);
944
997
  return () => {
@@ -953,7 +1006,6 @@
953
1006
  };
954
1007
 
955
1008
  const currentRouteData = findRoute(routes, location) || {};
956
-
957
1009
  const query = useQuery();
958
1010
 
959
1011
  const contextValue = {
@@ -967,9 +1019,7 @@
967
1019
  return createElement(
968
1020
  RouterContext.Provider,
969
1021
  { value: contextValue },
970
- Fragment({
971
- children: children,
972
- }),
1022
+ Fragment({ children }),
973
1023
  )
974
1024
  };
975
1025
 
@@ -1000,7 +1050,6 @@
1000
1050
 
1001
1051
  const NavLink = ({ to, exact = false, ...props }) => {
1002
1052
  const { location, navigate } = useRouter();
1003
-
1004
1053
  const isActive = exact ? location === to : location.startsWith(to);
1005
1054
 
1006
1055
  const resolveClass = (cls) =>
@@ -1012,7 +1061,6 @@
1012
1061
  };
1013
1062
 
1014
1063
  const classAttrName = props['ryunix-class'] ? 'ryunix-class' : 'className';
1015
-
1016
1064
  const classAttrValue = resolveClass(
1017
1065
  props['ryunix-class'] || props['className'],
1018
1066
  );
@@ -1035,77 +1083,6 @@
1035
1083
  )
1036
1084
  };
1037
1085
 
1038
- /**
1039
- * useMetadata: Hook to dynamically manage SEO metadata in the <head>.
1040
- * Supports title with template, description, robots, robots, canonical, OpenGraph, Twitter, and any standard meta.
1041
- * @param {Object} tags - Object with metatags to insert/update.
1042
- * @param {Object} options - Optional. Allows to define template and default for the title.
1043
-
1044
- */
1045
-
1046
- const useMetadata = (tags = {}, options = {}) => {
1047
- useEffect(() => {
1048
- if (typeof document === 'undefined') return // SSR safe
1049
-
1050
- let finalTitle = '';
1051
- let template = undefined;
1052
- let defaultTitle = 'Ryunix App';
1053
- if (options.title && typeof options.title === 'object') {
1054
- template = options.title.template;
1055
- if (typeof options.title.prefix === 'string') {
1056
- defaultTitle = options.title.prefix;
1057
- }
1058
- }
1059
-
1060
- // pageTitle tiene prioridad sobre title
1061
- let pageTitle = tags.pageTitle || tags.title;
1062
-
1063
- if (typeof pageTitle === 'string') {
1064
- if (pageTitle.trim() === '') {
1065
- finalTitle = defaultTitle;
1066
- } else if (template && template.includes('%s')) {
1067
- finalTitle = template.replace('%s', pageTitle);
1068
- } else {
1069
- finalTitle = pageTitle;
1070
- }
1071
- } else if (typeof pageTitle === 'object' && pageTitle !== null) {
1072
- finalTitle = defaultTitle;
1073
- } else if (!pageTitle) {
1074
- finalTitle = defaultTitle;
1075
- }
1076
- document.title = finalTitle;
1077
- // Canonical
1078
- if (tags.canonical) {
1079
- let link = document.querySelector('link[rel="canonical"]');
1080
- if (!link) {
1081
- link = document.createElement('link');
1082
- link.setAttribute('rel', 'canonical');
1083
- document.head.appendChild(link);
1084
- }
1085
- link.setAttribute('href', tags.canonical);
1086
- }
1087
- // Meta tags
1088
- Object.entries(tags).forEach(([key, value]) => {
1089
- if (key === 'title' || key === 'pageTitle' || key === 'canonical') return
1090
- let selector = `meta[name='${key}']`;
1091
- if (key.startsWith('og:') || key.startsWith('twitter:')) {
1092
- selector = `meta[property='${key}']`;
1093
- }
1094
- let meta = document.head.querySelector(selector);
1095
- if (!meta) {
1096
- meta = document.createElement('meta');
1097
- if (key.startsWith('og:') || key.startsWith('twitter:')) {
1098
- meta.setAttribute('property', key);
1099
- } else {
1100
- meta.setAttribute('name', key);
1101
- }
1102
- document.head.appendChild(meta);
1103
- }
1104
- meta.setAttribute('content', value);
1105
- });
1106
- }, [JSON.stringify(tags), JSON.stringify(options)]);
1107
- };
1108
-
1109
1086
  var Hooks = /*#__PURE__*/Object.freeze({
1110
1087
  __proto__: null,
1111
1088
  Children: Children,
@@ -1118,17 +1095,126 @@
1118
1095
  useMemo: useMemo,
1119
1096
  useMetadata: useMetadata,
1120
1097
  useQuery: useQuery,
1098
+ useReducer: useReducer,
1121
1099
  useRef: useRef,
1122
1100
  useRouter: useRouter,
1123
1101
  useStore: useStore
1124
1102
  });
1125
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.*
1126
1205
  var Ryunix = {
1127
1206
  createElement,
1128
1207
  render,
1129
1208
  init,
1130
1209
  Fragment,
1131
1210
  Hooks,
1211
+
1212
+ memo,
1213
+ lazy,
1214
+ Suspense,
1215
+
1216
+ safeRender,
1217
+ batchUpdates,
1132
1218
  };
1133
1219
 
1134
1220
  window.Ryunix = Ryunix;
@@ -1145,6 +1231,7 @@
1145
1231
  exports.useMemo = useMemo;
1146
1232
  exports.useMetadata = useMetadata;
1147
1233
  exports.useQuery = useQuery;
1234
+ exports.useReducer = useReducer;
1148
1235
  exports.useRef = useRef;
1149
1236
  exports.useRouter = useRouter;
1150
1237
  exports.useStore = useStore;