@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 +677 -590
- package/dist/Ryunix.min.js +1 -1
- package/package.json +1 -1
package/dist/Ryunix.js
CHANGED
|
@@ -1,105 +1,115 @@
|
|
|
1
1
|
(function (global, factory) {
|
|
2
|
-
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports
|
|
3
|
-
typeof define === 'function' && define.amd ? define(['exports'
|
|
4
|
-
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Ryunix = {}
|
|
5
|
-
})(this, (function (exports
|
|
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
|
-
|
|
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:
|
|
15
|
+
deletions: [],
|
|
13
16
|
wipFiber: null,
|
|
14
|
-
hookIndex:
|
|
15
|
-
effects:
|
|
16
|
-
};
|
|
17
|
+
hookIndex: 0,
|
|
18
|
+
effects: [],
|
|
19
|
+
});
|
|
17
20
|
|
|
18
|
-
|
|
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')
|
|
22
|
-
|
|
23
|
-
RYUNIX_EFFECT: Symbol('ryunix.effect')
|
|
24
|
-
RYUNIX_MEMO: Symbol('ryunix.memo')
|
|
25
|
-
RYUNIX_URL_QUERY: Symbol('ryunix.urlQuery')
|
|
26
|
-
RYUNIX_REF: Symbol('ryunix.ref')
|
|
27
|
-
RYUNIX_STORE: Symbol('ryunix.store')
|
|
28
|
-
RYUNIX_REDUCE: Symbol('ryunix.reduce')
|
|
29
|
-
RYUNIX_FRAGMENT: Symbol('ryunix.fragment')
|
|
30
|
-
RYUNIX_CONTEXT: Symbol('ryunix.context')
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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')
|
|
50
|
-
UPDATE: Symbol('ryunix.reconciler.status.update')
|
|
51
|
-
DELETION: Symbol('ryunix.reconciler.status.deletion')
|
|
52
|
-
NO_EFFECT: Symbol('ryunix.reconciler.status.
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
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
|
-
*
|
|
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
|
|
98
|
+
type,
|
|
96
99
|
props: {
|
|
97
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
*
|
|
122
|
-
* @param fiber -
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
*
|
|
139
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
170
|
-
* @param fiber -
|
|
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
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
hook.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
229
|
+
// Run new effect
|
|
230
|
+
try {
|
|
231
|
+
const cleanup = hook.effect();
|
|
190
232
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
*
|
|
202
|
-
* @param
|
|
203
|
-
*
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
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
|
-
|
|
330
|
+
let dom;
|
|
220
331
|
|
|
221
|
-
|
|
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
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* @param
|
|
228
|
-
* @param
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
460
|
-
|
|
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
|
-
|
|
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 (
|
|
549
|
-
|
|
593
|
+
while (state.nextUnitOfWork && !shouldYield) {
|
|
594
|
+
state.nextUnitOfWork = performUnitOfWork(state.nextUnitOfWork);
|
|
550
595
|
shouldYield = deadline.timeRemaining() < 1;
|
|
551
596
|
}
|
|
552
597
|
|
|
553
|
-
if (!
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
637
|
+
const state = getState();
|
|
638
|
+
state.wipRoot = {
|
|
613
639
|
dom: container,
|
|
614
640
|
props: {
|
|
615
641
|
children: [element],
|
|
616
642
|
},
|
|
617
|
-
alternate:
|
|
643
|
+
alternate: state.currentRoot,
|
|
618
644
|
};
|
|
619
645
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
scheduleWork(
|
|
623
|
-
return
|
|
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
|
-
|
|
653
|
+
const state = getState();
|
|
654
|
+
state.containerRoot = document.getElementById(root);
|
|
655
|
+
const renderProcess = render(MainElement, state.containerRoot);
|
|
656
|
+
return renderProcess
|
|
657
|
+
};
|
|
637
658
|
|
|
638
|
-
|
|
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
|
-
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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:
|
|
703
|
+
hookID: hookIndex,
|
|
680
704
|
type: RYUNIX_TYPES.RYUNIX_STORE,
|
|
681
705
|
state: oldHook ? oldHook.state : init ? init(initialState) : initialState,
|
|
682
|
-
queue:
|
|
706
|
+
queue: [], // Siempre nueva cola vacía
|
|
683
707
|
};
|
|
684
708
|
|
|
685
|
-
|
|
709
|
+
// Procesar acciones del render anterior
|
|
710
|
+
if (oldHook?.queue) {
|
|
686
711
|
oldHook.queue.forEach((action) => {
|
|
687
|
-
|
|
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
|
-
|
|
693
|
-
|
|
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
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
732
|
+
const currentState = getState();
|
|
733
|
+
currentState.wipRoot = {
|
|
734
|
+
dom: currentState.currentRoot.dom,
|
|
735
|
+
props: currentState.currentRoot.props,
|
|
736
|
+
alternate: currentState.currentRoot,
|
|
700
737
|
};
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
scheduleWork(
|
|
738
|
+
currentState.deletions = [];
|
|
739
|
+
currentState.hookIndex = 0;
|
|
740
|
+
scheduleWork(currentState.wipRoot);
|
|
704
741
|
};
|
|
705
742
|
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
743
|
-
|
|
771
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
772
|
+
state.hookIndex++;
|
|
744
773
|
};
|
|
745
774
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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:
|
|
785
|
+
value: oldHook ? oldHook.value : { current: initialValue },
|
|
764
786
|
};
|
|
765
787
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
788
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
789
|
+
state.hookIndex++;
|
|
769
790
|
return hook.value
|
|
770
791
|
};
|
|
771
792
|
|
|
772
|
-
|
|
773
|
-
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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
|
-
|
|
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
|
-
|
|
808
|
-
|
|
821
|
+
const hook = {
|
|
822
|
+
hookID: hookIndex,
|
|
823
|
+
type: RYUNIX_TYPES.RYUNIX_MEMO,
|
|
824
|
+
value,
|
|
825
|
+
deps,
|
|
826
|
+
};
|
|
809
827
|
|
|
810
|
-
|
|
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
|
-
|
|
833
|
-
|
|
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 =
|
|
840
|
-
|
|
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.
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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 (
|
|
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;
|