@unsetsoft/ryunixjs 1.2.2 → 1.2.3-canary.1
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 +951 -547
- 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
|
+
});
|
|
20
|
+
|
|
21
|
+
// Singleton for backward compatibility, but allows testing with isolated instances
|
|
22
|
+
let globalState = createRenderState();
|
|
17
23
|
|
|
18
|
-
const
|
|
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,353 @@
|
|
|
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;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Convert camelCase to kebab-case for CSS properties
|
|
254
|
+
* @param {string} camelCase - CamelCase string
|
|
255
|
+
* @returns {string} Kebab-case string
|
|
256
|
+
*/
|
|
257
|
+
const camelToKebab = (camelCase) => {
|
|
258
|
+
return camelCase.replace(
|
|
259
|
+
CAMEL_TO_KEBAB_REGEX,
|
|
260
|
+
(match) => `-${match.toLowerCase()}`,
|
|
261
|
+
)
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Apply styles to DOM element
|
|
266
|
+
* @param {HTMLElement} dom - DOM element
|
|
267
|
+
* @param {Object} styleObj - Style object
|
|
268
|
+
*/
|
|
269
|
+
const applyStyles = (dom, styleObj) => {
|
|
270
|
+
if (!is.object(styleObj) || is.null(styleObj)) {
|
|
271
|
+
dom.style.cssText = '';
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const cssText = Object.entries(styleObj)
|
|
277
|
+
.filter(([_, value]) => value != null) // Filter out null/undefined
|
|
278
|
+
.map(([key, value]) => {
|
|
279
|
+
const kebabKey = camelToKebab(key);
|
|
280
|
+
return `${kebabKey}: ${value}`
|
|
281
|
+
})
|
|
282
|
+
.join('; ');
|
|
283
|
+
|
|
284
|
+
dom.style.cssText = cssText;
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
287
|
+
console.error('Error applying styles:', error);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Apply CSS classes to DOM element
|
|
294
|
+
* @param {HTMLElement} dom - DOM element
|
|
295
|
+
* @param {string} prevClasses - Previous class string
|
|
296
|
+
* @param {string} nextClasses - Next class string
|
|
297
|
+
*/
|
|
298
|
+
const applyClasses = (dom, prevClasses, nextClasses) => {
|
|
299
|
+
// Allow empty/undefined - just remove classes
|
|
300
|
+
if (!nextClasses || nextClasses.trim() === '') {
|
|
301
|
+
if (prevClasses) {
|
|
302
|
+
const oldClasses = prevClasses.split(/\s+/).filter(Boolean);
|
|
303
|
+
dom.classList.remove(...oldClasses);
|
|
196
304
|
}
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Remove old classes
|
|
309
|
+
if (prevClasses) {
|
|
310
|
+
const oldClasses = prevClasses.split(/\s+/).filter(Boolean);
|
|
311
|
+
dom.classList.remove(...oldClasses);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Add new classes
|
|
315
|
+
const newClasses = nextClasses.split(/\s+/).filter(Boolean);
|
|
316
|
+
if (newClasses.length > 0) {
|
|
317
|
+
dom.classList.add(...newClasses);
|
|
197
318
|
}
|
|
198
319
|
};
|
|
199
320
|
|
|
200
321
|
/**
|
|
201
|
-
*
|
|
202
|
-
* @param fiber -
|
|
203
|
-
*
|
|
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
|
|
322
|
+
* Create a DOM element from fiber
|
|
323
|
+
* @param {Object} fiber - Fiber node
|
|
324
|
+
* @returns {HTMLElement|Text|null}
|
|
209
325
|
*/
|
|
210
326
|
const createDom = (fiber) => {
|
|
327
|
+
// Fragments don't create real DOM nodes
|
|
211
328
|
if (fiber.type === RYUNIX_TYPES.RYUNIX_FRAGMENT) {
|
|
212
|
-
return null
|
|
329
|
+
return null
|
|
213
330
|
}
|
|
214
|
-
const dom =
|
|
215
|
-
fiber.type == RYUNIX_TYPES.TEXT_ELEMENT
|
|
216
|
-
? document.createTextNode('')
|
|
217
|
-
: document.createElement(fiber.type);
|
|
218
331
|
|
|
219
|
-
|
|
332
|
+
let dom;
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
if (fiber.type === RYUNIX_TYPES.TEXT_ELEMENT) {
|
|
336
|
+
dom = document.createTextNode('');
|
|
337
|
+
} else if (is.string(fiber.type)) {
|
|
338
|
+
dom = document.createElement(fiber.type);
|
|
339
|
+
} else {
|
|
340
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
341
|
+
console.warn(
|
|
342
|
+
'Attempted to create DOM for non-host component:',
|
|
343
|
+
fiber.type,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return null
|
|
347
|
+
}
|
|
220
348
|
|
|
221
|
-
|
|
349
|
+
updateDom(dom, {}, fiber.props);
|
|
350
|
+
return dom
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
353
|
+
console.error('Error creating DOM element:', error, fiber);
|
|
354
|
+
}
|
|
355
|
+
return null
|
|
356
|
+
}
|
|
222
357
|
};
|
|
223
358
|
|
|
224
359
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* @param
|
|
228
|
-
* @param
|
|
229
|
-
* @param nextProps - An object containing the new props that need to be updated in the DOM.
|
|
360
|
+
* Update DOM element with new props
|
|
361
|
+
* @param {HTMLElement|Text} dom - DOM element
|
|
362
|
+
* @param {Object} prevProps - Previous props
|
|
363
|
+
* @param {Object} nextProps - Next props
|
|
230
364
|
*/
|
|
231
|
-
const updateDom = (dom, prevProps, nextProps) => {
|
|
365
|
+
const updateDom = (dom, prevProps = {}, nextProps = {}) => {
|
|
366
|
+
// Remove old event listeners
|
|
232
367
|
Object.keys(prevProps)
|
|
233
368
|
.filter(isEvent)
|
|
234
369
|
.filter((key) => isGone(nextProps)(key) || isNew(prevProps, nextProps)(key))
|
|
235
370
|
.forEach((name) => {
|
|
236
371
|
const eventType = name.toLowerCase().substring(2);
|
|
237
|
-
|
|
372
|
+
try {
|
|
373
|
+
dom.removeEventListener(eventType, prevProps[name]);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
376
|
+
console.warn('Error removing event listener:', error);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
238
379
|
});
|
|
239
380
|
|
|
381
|
+
// Remove old properties
|
|
240
382
|
Object.keys(prevProps)
|
|
241
383
|
.filter(isProperty)
|
|
242
384
|
.filter(isGone(nextProps))
|
|
243
385
|
.forEach((name) => {
|
|
386
|
+
// Skip special properties
|
|
387
|
+
if (
|
|
388
|
+
[
|
|
389
|
+
STRINGS.STYLE,
|
|
390
|
+
OLD_STRINGS.STYLE,
|
|
391
|
+
STRINGS.CLASS_NAME,
|
|
392
|
+
OLD_STRINGS.CLASS_NAME,
|
|
393
|
+
].includes(name)
|
|
394
|
+
) {
|
|
395
|
+
return
|
|
396
|
+
}
|
|
244
397
|
dom[name] = '';
|
|
245
398
|
});
|
|
246
399
|
|
|
400
|
+
// Set new properties
|
|
247
401
|
Object.keys(nextProps)
|
|
248
402
|
.filter(isProperty)
|
|
249
403
|
.filter(isNew(prevProps, nextProps))
|
|
250
404
|
.forEach((name) => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (nextProps['ryunix-class'] === '') {
|
|
257
|
-
throw new Error('data-class cannot be empty.')
|
|
405
|
+
try {
|
|
406
|
+
// Handle style properties
|
|
407
|
+
if (name === STRINGS.STYLE || name === OLD_STRINGS.STYLE) {
|
|
408
|
+
const styleValue = nextProps[name];
|
|
409
|
+
applyStyles(dom, styleValue);
|
|
258
410
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
411
|
+
// Handle className properties
|
|
412
|
+
else if (name === STRINGS.CLASS_NAME) {
|
|
413
|
+
applyClasses(
|
|
414
|
+
dom,
|
|
415
|
+
prevProps[STRINGS.CLASS_NAME],
|
|
416
|
+
nextProps[STRINGS.CLASS_NAME],
|
|
263
417
|
);
|
|
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) || []),
|
|
418
|
+
} else if (name === OLD_STRINGS.CLASS_NAME) {
|
|
419
|
+
applyClasses(
|
|
420
|
+
dom,
|
|
421
|
+
prevProps[OLD_STRINGS.CLASS_NAME],
|
|
422
|
+
nextProps[OLD_STRINGS.CLASS_NAME],
|
|
275
423
|
);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
424
|
+
}
|
|
425
|
+
// Handle other properties
|
|
426
|
+
else {
|
|
427
|
+
// Special handling for value and checked (controlled components)
|
|
428
|
+
if (name === 'value' || name === 'checked') {
|
|
429
|
+
if (dom[name] !== nextProps[name]) {
|
|
430
|
+
dom[name] = nextProps[name];
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
dom[name] = nextProps[name];
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} catch (error) {
|
|
437
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
438
|
+
console.warn(`Error setting property ${name}:`, error);
|
|
439
|
+
}
|
|
279
440
|
}
|
|
280
441
|
});
|
|
281
442
|
|
|
443
|
+
// Add new event listeners
|
|
282
444
|
Object.keys(nextProps)
|
|
283
445
|
.filter(isEvent)
|
|
284
446
|
.filter(isNew(prevProps, nextProps))
|
|
285
447
|
.forEach((name) => {
|
|
286
448
|
const eventType = name.toLowerCase().substring(2);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
return '-' + v.toLowerCase()
|
|
449
|
+
try {
|
|
450
|
+
dom.addEventListener(eventType, nextProps[name]);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
453
|
+
console.warn('Error adding event listener:', error);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
295
456
|
});
|
|
296
|
-
acc += `${key}: ${style[styleName]};`;
|
|
297
|
-
return acc
|
|
298
|
-
}, '');
|
|
299
457
|
};
|
|
300
458
|
|
|
301
|
-
/**
|
|
302
|
-
* The function commits changes made to the virtual DOM to the actual DOM.
|
|
303
|
-
*/
|
|
304
459
|
const commitRoot = () => {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
460
|
+
const state = getState();
|
|
461
|
+
state.deletions.forEach(commitWork);
|
|
462
|
+
commitWork(state.wipRoot.child);
|
|
463
|
+
state.currentRoot = state.wipRoot;
|
|
464
|
+
state.wipRoot = null;
|
|
309
465
|
};
|
|
310
466
|
|
|
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
467
|
const commitWork = (fiber) => {
|
|
319
468
|
if (!fiber) {
|
|
320
469
|
return
|
|
@@ -347,13 +496,6 @@
|
|
|
347
496
|
commitWork(fiber.sibling);
|
|
348
497
|
};
|
|
349
498
|
|
|
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
499
|
const commitDeletion = (fiber, domParent) => {
|
|
358
500
|
if (fiber.dom) {
|
|
359
501
|
domParent.removeChild(fiber.dom);
|
|
@@ -367,37 +509,54 @@
|
|
|
367
509
|
};
|
|
368
510
|
|
|
369
511
|
/**
|
|
370
|
-
*
|
|
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
|
|
512
|
+
* Reconcile children with key optimization
|
|
377
513
|
*/
|
|
378
514
|
const reconcileChildren = (wipFiber, elements) => {
|
|
515
|
+
const state = getState();
|
|
379
516
|
let index = 0;
|
|
380
|
-
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
|
|
381
517
|
let prevSibling;
|
|
382
518
|
|
|
383
|
-
|
|
519
|
+
// Build map of old fibers by key/index
|
|
520
|
+
const oldFiberMap = new Map();
|
|
521
|
+
let oldFiber = wipFiber.alternate?.child;
|
|
522
|
+
let position = 0;
|
|
523
|
+
|
|
524
|
+
while (oldFiber) {
|
|
525
|
+
const key = oldFiber.key ?? `__index_${position}__`;
|
|
526
|
+
oldFiberMap.set(key, oldFiber);
|
|
527
|
+
oldFiber = oldFiber.sibling;
|
|
528
|
+
position++;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Process new elements
|
|
532
|
+
while (index < elements.length) {
|
|
384
533
|
const element = elements[index];
|
|
385
|
-
|
|
534
|
+
if (!element) {
|
|
535
|
+
index++;
|
|
536
|
+
continue
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const key = element.key ?? `__index_${index}__`;
|
|
540
|
+
const matchedFiber = oldFiberMap.get(key);
|
|
386
541
|
|
|
387
|
-
|
|
542
|
+
let newFiber;
|
|
543
|
+
const sameType = matchedFiber && element.type === matchedFiber.type;
|
|
388
544
|
|
|
389
545
|
if (sameType) {
|
|
546
|
+
// Update existing fiber
|
|
390
547
|
newFiber = {
|
|
391
|
-
type:
|
|
548
|
+
type: matchedFiber.type,
|
|
392
549
|
props: element.props,
|
|
393
|
-
dom:
|
|
550
|
+
dom: matchedFiber.dom,
|
|
394
551
|
parent: wipFiber,
|
|
395
|
-
alternate:
|
|
552
|
+
alternate: matchedFiber,
|
|
396
553
|
effectTag: EFFECT_TAGS.UPDATE,
|
|
397
|
-
hooks:
|
|
554
|
+
hooks: matchedFiber.hooks,
|
|
555
|
+
key: element.key,
|
|
398
556
|
};
|
|
399
|
-
|
|
400
|
-
|
|
557
|
+
oldFiberMap.delete(key);
|
|
558
|
+
} else {
|
|
559
|
+
// Create new fiber
|
|
401
560
|
newFiber = {
|
|
402
561
|
type: element.type,
|
|
403
562
|
props: element.props,
|
|
@@ -405,42 +564,43 @@
|
|
|
405
564
|
parent: wipFiber,
|
|
406
565
|
alternate: null,
|
|
407
566
|
effectTag: EFFECT_TAGS.PLACEMENT,
|
|
567
|
+
key: element.key,
|
|
408
568
|
};
|
|
409
|
-
}
|
|
410
|
-
if (oldFiber && !sameType) {
|
|
411
|
-
oldFiber.effectTag = EFFECT_TAGS.DELETION;
|
|
412
|
-
vars.deletions.push(oldFiber);
|
|
413
|
-
}
|
|
414
569
|
|
|
415
|
-
|
|
416
|
-
|
|
570
|
+
// Mark matched fiber for deletion if exists
|
|
571
|
+
if (matchedFiber) {
|
|
572
|
+
matchedFiber.effectTag = EFFECT_TAGS.DELETION;
|
|
573
|
+
state.deletions.push(matchedFiber);
|
|
574
|
+
oldFiberMap.delete(key);
|
|
575
|
+
}
|
|
417
576
|
}
|
|
418
577
|
|
|
578
|
+
// Link fibers
|
|
419
579
|
if (index === 0) {
|
|
420
580
|
wipFiber.child = newFiber;
|
|
421
|
-
} else if (
|
|
581
|
+
} else if (newFiber) {
|
|
422
582
|
prevSibling.sibling = newFiber;
|
|
423
583
|
}
|
|
424
584
|
|
|
425
585
|
prevSibling = newFiber;
|
|
426
586
|
index++;
|
|
427
587
|
}
|
|
588
|
+
|
|
589
|
+
// Delete remaining old fibers
|
|
590
|
+
oldFiberMap.forEach((fiber) => {
|
|
591
|
+
fiber.effectTag = EFFECT_TAGS.DELETION;
|
|
592
|
+
state.deletions.push(fiber);
|
|
593
|
+
});
|
|
428
594
|
};
|
|
429
595
|
|
|
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
596
|
const updateFunctionComponent = (fiber) => {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
597
|
+
const state = getState();
|
|
598
|
+
state.wipFiber = fiber;
|
|
599
|
+
state.hookIndex = 0;
|
|
600
|
+
state.wipFiber.hooks = [];
|
|
601
|
+
|
|
441
602
|
const children = [fiber.type(fiber.props)];
|
|
442
603
|
|
|
443
|
-
// Aquí detectamos si es Provider para guardar contexto y valor en fiber
|
|
444
604
|
if (fiber.type._contextId && fiber.props.value !== undefined) {
|
|
445
605
|
fiber._contextId = fiber.type._contextId;
|
|
446
606
|
fiber._contextValue = fiber.props.value;
|
|
@@ -449,62 +609,186 @@
|
|
|
449
609
|
reconcileChildren(fiber, children);
|
|
450
610
|
};
|
|
451
611
|
|
|
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
612
|
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);
|
|
613
|
+
if (!fiber.dom) {
|
|
614
|
+
fiber.dom = createDom(fiber);
|
|
470
615
|
}
|
|
616
|
+
const children = fiber.props?.children || [];
|
|
617
|
+
reconcileChildren(fiber, children);
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const Image = ({ src, ...props }) => {
|
|
621
|
+
return createElement('img', { ...props, src })
|
|
471
622
|
};
|
|
472
623
|
|
|
473
624
|
/**
|
|
474
|
-
*
|
|
475
|
-
* @returns An `<img>` element is being returned with the specified `src` and other props passed to the
|
|
476
|
-
* `Image` component. The `src` is either the original `src` value or the result of calling
|
|
477
|
-
* `optimizationImageApi` function with `src` and `props` if `optimization` is set to 'true'.
|
|
625
|
+
* Priority levels for updates
|
|
478
626
|
*/
|
|
479
|
-
const
|
|
480
|
-
|
|
627
|
+
const Priority = {
|
|
628
|
+
IMMEDIATE: 1, // User input (clicks, typing)
|
|
629
|
+
USER_BLOCKING: 2, // Hover, scroll
|
|
630
|
+
NORMAL: 3, // Data fetching
|
|
631
|
+
LOW: 4, // Analytics
|
|
632
|
+
IDLE: 5, // Background tasks
|
|
633
|
+
};
|
|
481
634
|
|
|
482
|
-
|
|
635
|
+
Priority.NORMAL;
|
|
483
636
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
637
|
+
/**
|
|
638
|
+
* Performance profiler for Ryunix
|
|
639
|
+
*/
|
|
640
|
+
class Profiler {
|
|
641
|
+
constructor() {
|
|
642
|
+
this.enabled = process.env.NODE_ENV !== 'production';
|
|
643
|
+
this.measures = new Map();
|
|
644
|
+
this.renderTimes = [];
|
|
645
|
+
this.maxSamples = 100;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
startMeasure(name) {
|
|
649
|
+
if (!this.enabled) return
|
|
650
|
+
this.measures.set(name, performance.now());
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
endMeasure(name) {
|
|
654
|
+
if (!this.enabled) return
|
|
655
|
+
const start = this.measures.get(name);
|
|
656
|
+
if (!start) return
|
|
657
|
+
|
|
658
|
+
const duration = performance.now() - start;
|
|
659
|
+
this.measures.delete(name);
|
|
660
|
+
|
|
661
|
+
return duration
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
recordRender(componentName, duration) {
|
|
665
|
+
if (!this.enabled) return
|
|
666
|
+
|
|
667
|
+
this.renderTimes.push({
|
|
668
|
+
component: componentName,
|
|
669
|
+
duration,
|
|
670
|
+
timestamp: Date.now(),
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
if (this.renderTimes.length > this.maxSamples) {
|
|
674
|
+
this.renderTimes.shift();
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
getStats() {
|
|
679
|
+
if (!this.enabled) return null
|
|
680
|
+
|
|
681
|
+
const total = this.renderTimes.reduce((sum, r) => sum + r.duration, 0);
|
|
682
|
+
const avg = total / this.renderTimes.length;
|
|
683
|
+
const max = Math.max(...this.renderTimes.map((r) => r.duration));
|
|
684
|
+
const min = Math.min(...this.renderTimes.map((r) => r.duration));
|
|
685
|
+
|
|
686
|
+
return { total, avg, max, min, count: this.renderTimes.length }
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
getSlowestComponents(limit = 10) {
|
|
690
|
+
if (!this.enabled) return []
|
|
691
|
+
|
|
692
|
+
const byComponent = new Map();
|
|
693
|
+
|
|
694
|
+
this.renderTimes.forEach(({ component, duration }) => {
|
|
695
|
+
if (!byComponent.has(component)) {
|
|
696
|
+
byComponent.set(component, { total: 0, count: 0, max: 0 });
|
|
697
|
+
}
|
|
698
|
+
const stats = byComponent.get(component);
|
|
699
|
+
stats.total += duration;
|
|
700
|
+
stats.count++;
|
|
701
|
+
stats.max = Math.max(stats.max, duration);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
return Array.from(byComponent.entries())
|
|
705
|
+
.map(([name, stats]) => ({
|
|
706
|
+
name,
|
|
707
|
+
avg: stats.total / stats.count,
|
|
708
|
+
max: stats.max,
|
|
709
|
+
count: stats.count,
|
|
710
|
+
}))
|
|
711
|
+
.sort((a, b) => b.avg - a.avg)
|
|
712
|
+
.slice(0, limit)
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
logStats() {
|
|
716
|
+
if (!this.enabled) return
|
|
717
|
+
|
|
718
|
+
const stats = this.getStats();
|
|
719
|
+
if (!stats) return
|
|
720
|
+
|
|
721
|
+
console.group('🔍 Ryunix Performance Stats');
|
|
722
|
+
console.log(`Total renders: ${stats.count}`);
|
|
723
|
+
console.log(`Avg render time: ${stats.avg.toFixed(2)}ms`);
|
|
724
|
+
console.log(
|
|
725
|
+
`Min: ${stats.min.toFixed(2)}ms | Max: ${stats.max.toFixed(2)}ms`,
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
const slowest = this.getSlowestComponents(5);
|
|
729
|
+
if (slowest.length > 0) {
|
|
730
|
+
console.log('\n⚠️ Slowest components:');
|
|
731
|
+
slowest.forEach((comp, i) => {
|
|
732
|
+
console.log(
|
|
733
|
+
`${i + 1}. ${comp.name}: ${comp.avg.toFixed(2)}ms avg (${comp.count} renders)`,
|
|
734
|
+
);
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
console.groupEnd();
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
clear() {
|
|
741
|
+
this.renderTimes = [];
|
|
742
|
+
this.measures.clear();
|
|
743
|
+
}
|
|
488
744
|
|
|
489
|
-
|
|
745
|
+
enable() {
|
|
746
|
+
this.enabled = true;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
disable() {
|
|
750
|
+
this.enabled = false;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Global profiler instance
|
|
755
|
+
const profiler = new Profiler();
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Hook to profile component render
|
|
759
|
+
*/
|
|
760
|
+
const useProfiler = (componentName) => {
|
|
761
|
+
const startTime = performance.now();
|
|
762
|
+
|
|
763
|
+
return () => {
|
|
764
|
+
const duration = performance.now() - startTime;
|
|
765
|
+
profiler.recordRender(componentName, duration);
|
|
766
|
+
}
|
|
490
767
|
};
|
|
491
768
|
|
|
492
769
|
/**
|
|
493
|
-
*
|
|
494
|
-
* browser needs to yield to other tasks.
|
|
495
|
-
* @param deadline - The `deadline` parameter is an object that represents the amount of time the
|
|
496
|
-
* browser has to perform work before it needs to handle other tasks. It has a `timeRemaining()` method
|
|
497
|
-
* that returns the amount of time remaining before the deadline is reached. The `shouldYield` variable
|
|
498
|
-
* is used to determine
|
|
770
|
+
* HOC to profile component
|
|
499
771
|
*/
|
|
772
|
+
const withProfiler = (Component, name) => {
|
|
773
|
+
return (props) => {
|
|
774
|
+
profiler.startMeasure(name);
|
|
775
|
+
const result = Component(props);
|
|
776
|
+
const duration = profiler.endMeasure(name);
|
|
777
|
+
if (duration) profiler.recordRender(name, duration);
|
|
778
|
+
return result
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
|
|
500
782
|
const workLoop = (deadline) => {
|
|
783
|
+
const state = getState();
|
|
501
784
|
let shouldYield = false;
|
|
502
|
-
|
|
503
|
-
|
|
785
|
+
|
|
786
|
+
while (state.nextUnitOfWork && !shouldYield) {
|
|
787
|
+
state.nextUnitOfWork = performUnitOfWork(state.nextUnitOfWork);
|
|
504
788
|
shouldYield = deadline.timeRemaining() < 1;
|
|
505
789
|
}
|
|
506
790
|
|
|
507
|
-
if (!
|
|
791
|
+
if (!state.nextUnitOfWork && state.wipRoot) {
|
|
508
792
|
commitRoot();
|
|
509
793
|
}
|
|
510
794
|
|
|
@@ -513,25 +797,21 @@
|
|
|
513
797
|
|
|
514
798
|
requestIdleCallback(workLoop);
|
|
515
799
|
|
|
516
|
-
/**
|
|
517
|
-
* The function performs a unit of work by updating either a function component or a host component and
|
|
518
|
-
* returns the next fiber to be processed.
|
|
519
|
-
* @param fiber - A fiber is a unit of work in Ryunix that represents a component and its state. It
|
|
520
|
-
* contains information about the component's type, props, and children, as well as pointers to its
|
|
521
|
-
* parent, child, and sibling fibers. The `performUnitOfWork` function takes a fiber as a parameter and
|
|
522
|
-
* performs work
|
|
523
|
-
* @returns The function `performUnitOfWork` returns the next fiber to be processed. If the current
|
|
524
|
-
* fiber has a child, it returns the child. Otherwise, it looks for the next sibling of the current
|
|
525
|
-
* fiber. If there are no more siblings, it goes up the tree to the parent and looks for the next
|
|
526
|
-
* sibling of the parent. The function returns `null` if there are no more fibers to process.
|
|
527
|
-
*/
|
|
528
800
|
const performUnitOfWork = (fiber) => {
|
|
801
|
+
const componentName = fiber.type?.name || fiber.type?.displayName || 'Unknown';
|
|
802
|
+
|
|
803
|
+
profiler.startMeasure(componentName);
|
|
804
|
+
|
|
529
805
|
const isFunctionComponent = fiber.type instanceof Function;
|
|
530
806
|
if (isFunctionComponent) {
|
|
531
807
|
updateFunctionComponent(fiber);
|
|
532
808
|
} else {
|
|
533
809
|
updateHostComponent(fiber);
|
|
534
810
|
}
|
|
811
|
+
|
|
812
|
+
const duration = profiler.endMeasure(componentName);
|
|
813
|
+
if (duration) profiler.recordRender(componentName, duration);
|
|
814
|
+
|
|
535
815
|
if (fiber.child) {
|
|
536
816
|
return fiber.child
|
|
537
817
|
}
|
|
@@ -544,237 +824,223 @@
|
|
|
544
824
|
}
|
|
545
825
|
};
|
|
546
826
|
|
|
547
|
-
const scheduleWork = (root) => {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
827
|
+
const scheduleWork = (root, priority = Priority.NORMAL) => {
|
|
828
|
+
const state = getState();
|
|
829
|
+
state.nextUnitOfWork = root;
|
|
830
|
+
state.wipRoot = root;
|
|
831
|
+
state.deletions = [];
|
|
832
|
+
state.hookIndex = 0;
|
|
833
|
+
state.effects = [];
|
|
834
|
+
|
|
835
|
+
// Higher priority = faster scheduling
|
|
836
|
+
if (priority <= Priority.USER_BLOCKING) {
|
|
837
|
+
requestIdleCallback(workLoop);
|
|
838
|
+
} else {
|
|
839
|
+
setTimeout(() => requestIdleCallback(workLoop), 0);
|
|
840
|
+
}
|
|
555
841
|
};
|
|
556
842
|
|
|
557
|
-
/**
|
|
558
|
-
* Renders an element into a container using a work-in-progress (WIP) root.
|
|
559
|
-
* @function render
|
|
560
|
-
* @param {Object|HTMLElement} element - The element to be rendered in the container. It can be a Ryunix component (custom element) or a standard DOM element.
|
|
561
|
-
* @param {HTMLElement} container - The container where the element will be rendered. This parameter is optional if `createRoot()` is used beforehand to set up the container.
|
|
562
|
-
* @description The function assigns the `container` to a work-in-progress root and sets up properties for reconciliation, including children and the reference to the current root.
|
|
563
|
-
* It also clears any scheduled deletions and establishes the next unit of work for incremental rendering.
|
|
564
|
-
*/
|
|
565
843
|
const render = (element, container) => {
|
|
566
|
-
|
|
844
|
+
const state = getState();
|
|
845
|
+
state.wipRoot = {
|
|
567
846
|
dom: container,
|
|
568
847
|
props: {
|
|
569
848
|
children: [element],
|
|
570
849
|
},
|
|
571
|
-
alternate:
|
|
850
|
+
alternate: state.currentRoot,
|
|
572
851
|
};
|
|
573
852
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
scheduleWork(
|
|
577
|
-
return
|
|
853
|
+
state.nextUnitOfWork = state.wipRoot;
|
|
854
|
+
state.deletions = [];
|
|
855
|
+
scheduleWork(state.wipRoot);
|
|
856
|
+
return state.wipRoot
|
|
578
857
|
};
|
|
579
858
|
|
|
580
|
-
/**
|
|
581
|
-
* Initializes the application by creating a reference to a DOM element with the specified ID and rendering the main component.
|
|
582
|
-
* @function init
|
|
583
|
-
* @param {Object} MainElement - The main component to render, typically the root component of the application.
|
|
584
|
-
* @param {string} [root='__ryunix'] - The ID of the HTML element that serves as the container for the root element. Defaults to `'__ryunix'` if not provided.
|
|
585
|
-
* @example
|
|
586
|
-
* Ryunix.init(App, "__ryunix"); // Initializes and renders the App component into the <div id="__ryunix"></div> element.
|
|
587
|
-
* @description This function retrieves the container element by its ID and invokes the `render` function to render the main component into it.
|
|
588
|
-
*/
|
|
589
859
|
const init = (MainElement, root = '__ryunix') => {
|
|
590
|
-
|
|
860
|
+
const state = getState();
|
|
861
|
+
state.containerRoot = document.getElementById(root);
|
|
862
|
+
const renderProcess = render(MainElement, state.containerRoot);
|
|
863
|
+
return renderProcess
|
|
864
|
+
};
|
|
591
865
|
|
|
592
|
-
|
|
866
|
+
const safeRender = (component, props, onError) => {
|
|
867
|
+
try {
|
|
868
|
+
return component(props)
|
|
869
|
+
} catch (error) {
|
|
870
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
871
|
+
console.error('Component error:', error);
|
|
872
|
+
}
|
|
873
|
+
if (onError) onError(error);
|
|
874
|
+
return null
|
|
875
|
+
}
|
|
876
|
+
};
|
|
593
877
|
|
|
594
|
-
|
|
878
|
+
const validateHookCall = () => {
|
|
879
|
+
const state = getState();
|
|
880
|
+
if (!state.wipFiber) {
|
|
881
|
+
throw new Error(
|
|
882
|
+
'Hooks can only be called inside the body of a function component.',
|
|
883
|
+
)
|
|
884
|
+
}
|
|
885
|
+
if (!Array.isArray(state.wipFiber.hooks)) {
|
|
886
|
+
state.wipFiber.hooks = [];
|
|
887
|
+
}
|
|
595
888
|
};
|
|
596
889
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
*/
|
|
603
|
-
const useStore = (initialState, init) => {
|
|
604
|
-
const reducer = (state, action) =>
|
|
605
|
-
typeof action === 'function' ? action(state) : action;
|
|
890
|
+
const haveDepsChanged = (oldDeps, newDeps) => {
|
|
891
|
+
if (!oldDeps || !newDeps) return true
|
|
892
|
+
if (oldDeps.length !== newDeps.length) return true
|
|
893
|
+
return oldDeps.some((dep, i) => !Object.is(dep, newDeps[i]))
|
|
894
|
+
};
|
|
606
895
|
|
|
607
|
-
|
|
896
|
+
const useStore = (initialState) => {
|
|
897
|
+
const reducer = (state, action) =>
|
|
898
|
+
is.function(action) ? action(state) : action;
|
|
899
|
+
return useReducer(reducer, initialState)
|
|
608
900
|
};
|
|
609
901
|
|
|
610
|
-
/**
|
|
611
|
-
* The `useReducer` function in JavaScript is used to manage state updates based on actions dispatched
|
|
612
|
-
* to a reducer function.
|
|
613
|
-
* @param reducer - The `reducer` parameter in the `useReducer` function is a function that takes the
|
|
614
|
-
* current state and an action as arguments, and returns the new state based on the action. It is used
|
|
615
|
-
* to update the state in response to different actions dispatched by the `dispatch` function.
|
|
616
|
-
* @param initialState - The `initialState` parameter in the `useReducer` function represents the
|
|
617
|
-
* initial state of the reducer. It is the state that will be used when the reducer is first
|
|
618
|
-
* initialized or reset. This initial state can be any value or object that the reducer will operate on
|
|
619
|
-
* and update based on the dispatched actions
|
|
620
|
-
* @param init - The `init` parameter in the `useReducer` function is an optional function that can be
|
|
621
|
-
* used to initialize the state. If provided, it will be called with the `initialState` as its argument
|
|
622
|
-
* and the return value will be used as the initial state value. If `init` is not
|
|
623
|
-
* @returns The `useReducer` function is returning an array with two elements: the current state and a
|
|
624
|
-
* dispatch function.
|
|
625
|
-
*/
|
|
626
902
|
const useReducer = (reducer, initialState, init) => {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
903
|
+
validateHookCall();
|
|
904
|
+
|
|
905
|
+
const state = getState();
|
|
906
|
+
const { wipFiber, hookIndex } = state;
|
|
907
|
+
const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
|
|
631
908
|
|
|
632
909
|
const hook = {
|
|
633
|
-
hookID:
|
|
910
|
+
hookID: hookIndex,
|
|
634
911
|
type: RYUNIX_TYPES.RYUNIX_STORE,
|
|
635
912
|
state: oldHook ? oldHook.state : init ? init(initialState) : initialState,
|
|
636
|
-
queue:
|
|
913
|
+
queue: [], // Siempre nueva cola vacía
|
|
637
914
|
};
|
|
638
915
|
|
|
639
|
-
|
|
916
|
+
// Procesar acciones del render anterior
|
|
917
|
+
if (oldHook?.queue) {
|
|
640
918
|
oldHook.queue.forEach((action) => {
|
|
641
|
-
|
|
919
|
+
try {
|
|
920
|
+
hook.state = reducer(hook.state, action);
|
|
921
|
+
} catch (error) {
|
|
922
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
923
|
+
console.error('Error in reducer:', error);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
642
926
|
});
|
|
643
927
|
}
|
|
644
928
|
|
|
645
929
|
const dispatch = (action) => {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
930
|
+
if (action === undefined) {
|
|
931
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
932
|
+
console.warn('dispatch called with undefined action');
|
|
933
|
+
}
|
|
934
|
+
return
|
|
935
|
+
}
|
|
649
936
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
937
|
+
hook.queue.push(action);
|
|
938
|
+
|
|
939
|
+
const currentState = getState();
|
|
940
|
+
currentState.wipRoot = {
|
|
941
|
+
dom: currentState.currentRoot.dom,
|
|
942
|
+
props: currentState.currentRoot.props,
|
|
943
|
+
alternate: currentState.currentRoot,
|
|
654
944
|
};
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
scheduleWork(
|
|
945
|
+
currentState.deletions = [];
|
|
946
|
+
currentState.hookIndex = 0;
|
|
947
|
+
scheduleWork(currentState.wipRoot);
|
|
658
948
|
};
|
|
659
949
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
vars.wipFiber.hooks[vars.hookIndex] = hook;
|
|
665
|
-
vars.hookIndex++;
|
|
666
|
-
|
|
950
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
951
|
+
state.hookIndex++;
|
|
667
952
|
return [hook.state, dispatch]
|
|
668
953
|
};
|
|
669
954
|
|
|
670
|
-
/**
|
|
671
|
-
* This is a function that creates a hook for managing side effects in Ryunix components.
|
|
672
|
-
* @param effect - The effect function that will be executed after the component has rendered or when
|
|
673
|
-
* the dependencies have changed. It can perform side effects such as fetching data, updating the DOM,
|
|
674
|
-
* or subscribing to events.
|
|
675
|
-
* @param deps - An array of dependencies that the effect depends on. If any of the dependencies change
|
|
676
|
-
* between renders, the effect will be re-run. If the array is empty, the effect will only run once on
|
|
677
|
-
* mount and never again.
|
|
678
|
-
*/
|
|
679
|
-
|
|
680
955
|
const useEffect = (callback, deps) => {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
956
|
+
validateHookCall();
|
|
957
|
+
|
|
958
|
+
if (!is.function(callback)) {
|
|
959
|
+
throw new Error('useEffect callback must be a function')
|
|
960
|
+
}
|
|
961
|
+
if (deps !== undefined && !Array.isArray(deps)) {
|
|
962
|
+
throw new Error('useEffect dependencies must be an array or undefined')
|
|
963
|
+
}
|
|
685
964
|
|
|
686
|
-
const
|
|
965
|
+
const state = getState();
|
|
966
|
+
const { wipFiber, hookIndex } = state;
|
|
967
|
+
const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
|
|
968
|
+
const hasChanged = haveDepsChanged(oldHook?.deps, deps);
|
|
687
969
|
|
|
688
970
|
const hook = {
|
|
689
|
-
hookID:
|
|
971
|
+
hookID: hookIndex,
|
|
690
972
|
type: RYUNIX_TYPES.RYUNIX_EFFECT,
|
|
691
973
|
deps,
|
|
692
974
|
effect: hasChanged ? callback : null,
|
|
693
975
|
cancel: oldHook?.cancel,
|
|
694
976
|
};
|
|
695
977
|
|
|
696
|
-
|
|
697
|
-
|
|
978
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
979
|
+
state.hookIndex++;
|
|
698
980
|
};
|
|
699
981
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
* @returns The `useRef` function is returning the `current` property of the `hook.value` object. This
|
|
707
|
-
* property contains the current value of the reference being managed by the `useRef` hook.
|
|
708
|
-
*/
|
|
709
|
-
const useRef = (initial) => {
|
|
710
|
-
const oldHook =
|
|
711
|
-
vars.wipFiber.alternate &&
|
|
712
|
-
vars.wipFiber.alternate.hooks &&
|
|
713
|
-
vars.wipFiber.alternate.hooks[vars.hookIndex];
|
|
982
|
+
const useRef = (initialValue) => {
|
|
983
|
+
validateHookCall();
|
|
984
|
+
|
|
985
|
+
const state = getState();
|
|
986
|
+
const { wipFiber, hookIndex } = state;
|
|
987
|
+
const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
|
|
714
988
|
|
|
715
989
|
const hook = {
|
|
990
|
+
hookID: hookIndex,
|
|
716
991
|
type: RYUNIX_TYPES.RYUNIX_REF,
|
|
717
|
-
value: oldHook ? oldHook.value : { current:
|
|
992
|
+
value: oldHook ? oldHook.value : { current: initialValue },
|
|
718
993
|
};
|
|
719
994
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
995
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
996
|
+
state.hookIndex++;
|
|
723
997
|
return hook.value
|
|
724
998
|
};
|
|
725
999
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
* dependencies provided.
|
|
729
|
-
* @param comp - The `comp` parameter in the `useMemo` function is a function that represents the
|
|
730
|
-
* computation that needs to be memoized. This function will be executed to calculate the memoized
|
|
731
|
-
* value based on the dependencies provided.
|
|
732
|
-
* @param deps - The `deps` parameter in the `useMemo` function stands for dependencies. It is an array
|
|
733
|
-
* of values that the function depends on. The `useMemo` function will only recompute the memoized
|
|
734
|
-
* value when one of the dependencies has changed.
|
|
735
|
-
* @returns The `useMemo` function returns the `value` property of the `hook` object, which is either
|
|
736
|
-
* the memoized value from the previous render if the dependencies have not changed, or the result of
|
|
737
|
-
* calling the `comp` function if the dependencies have changed.
|
|
738
|
-
*/
|
|
739
|
-
const useMemo = (comp, deps) => {
|
|
740
|
-
const oldHook =
|
|
741
|
-
vars.wipFiber.alternate &&
|
|
742
|
-
vars.wipFiber.alternate.hooks &&
|
|
743
|
-
vars.wipFiber.alternate.hooks[vars.hookIndex];
|
|
1000
|
+
const useMemo = (compute, deps) => {
|
|
1001
|
+
validateHookCall();
|
|
744
1002
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1003
|
+
if (!is.function(compute)) {
|
|
1004
|
+
throw new Error('useMemo callback must be a function')
|
|
1005
|
+
}
|
|
1006
|
+
if (!Array.isArray(deps)) {
|
|
1007
|
+
throw new Error('useMemo requires a dependencies array')
|
|
1008
|
+
}
|
|
750
1009
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1010
|
+
const state = getState();
|
|
1011
|
+
const { wipFiber, hookIndex } = state;
|
|
1012
|
+
const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
|
|
1013
|
+
|
|
1014
|
+
let value;
|
|
1015
|
+
if (oldHook && !haveDepsChanged(oldHook.deps, deps)) {
|
|
1016
|
+
value = oldHook.value;
|
|
757
1017
|
} else {
|
|
758
|
-
|
|
1018
|
+
try {
|
|
1019
|
+
value = compute();
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
1022
|
+
console.error('Error in useMemo computation:', error);
|
|
1023
|
+
}
|
|
1024
|
+
value = undefined;
|
|
1025
|
+
}
|
|
759
1026
|
}
|
|
760
1027
|
|
|
761
|
-
|
|
762
|
-
|
|
1028
|
+
const hook = {
|
|
1029
|
+
hookID: hookIndex,
|
|
1030
|
+
type: RYUNIX_TYPES.RYUNIX_MEMO,
|
|
1031
|
+
value,
|
|
1032
|
+
deps,
|
|
1033
|
+
};
|
|
763
1034
|
|
|
764
|
-
|
|
1035
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
1036
|
+
state.hookIndex++;
|
|
1037
|
+
return value
|
|
765
1038
|
};
|
|
766
1039
|
|
|
767
|
-
/**
|
|
768
|
-
* The useCallback function in JavaScript returns a memoized version of the callback function that only
|
|
769
|
-
* changes if one of the dependencies has changed.
|
|
770
|
-
* @param callback - The `callback` parameter is a function that you want to memoize using
|
|
771
|
-
* `useCallback`. This function will only be re-created if any of the dependencies specified in the
|
|
772
|
-
* `deps` array change.
|
|
773
|
-
* @param deps - Dependencies array that the callback function depends on.
|
|
774
|
-
* @returns The useCallback function is returning a memoized version of the callback function. It is
|
|
775
|
-
* using the useMemo hook to memoize the callback function based on the provided dependencies (deps).
|
|
776
|
-
*/
|
|
777
1040
|
const useCallback = (callback, deps) => {
|
|
1041
|
+
if (!is.function(callback)) {
|
|
1042
|
+
throw new Error('useCallback requires a function as first argument')
|
|
1043
|
+
}
|
|
778
1044
|
return useMemo(() => callback, deps)
|
|
779
1045
|
};
|
|
780
1046
|
|
|
@@ -782,55 +1048,109 @@
|
|
|
782
1048
|
contextId = RYUNIX_TYPES.RYUNIX_CONTEXT,
|
|
783
1049
|
defaultValue = {},
|
|
784
1050
|
) => {
|
|
785
|
-
const Provider = ({ children }) => {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1051
|
+
const Provider = ({ children, value }) => {
|
|
1052
|
+
const element = Fragment({ children });
|
|
1053
|
+
element._contextId = contextId;
|
|
1054
|
+
element._contextValue = value;
|
|
1055
|
+
return element
|
|
789
1056
|
};
|
|
790
1057
|
|
|
791
1058
|
Provider._contextId = contextId;
|
|
792
1059
|
|
|
793
|
-
const useContext = (ctxID =
|
|
794
|
-
|
|
1060
|
+
const useContext = (ctxID = contextId) => {
|
|
1061
|
+
validateHookCall();
|
|
1062
|
+
|
|
1063
|
+
const state = getState();
|
|
1064
|
+
let fiber = state.wipFiber;
|
|
1065
|
+
|
|
795
1066
|
while (fiber) {
|
|
796
|
-
if (fiber.
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1067
|
+
if (fiber._contextId === ctxID && fiber._contextValue !== undefined) {
|
|
1068
|
+
return fiber._contextValue
|
|
1069
|
+
}
|
|
1070
|
+
if (
|
|
1071
|
+
fiber.type?._contextId === ctxID &&
|
|
1072
|
+
fiber.props?.value !== undefined
|
|
1073
|
+
) {
|
|
1074
|
+
return fiber.props.value
|
|
801
1075
|
}
|
|
802
1076
|
fiber = fiber.parent;
|
|
803
1077
|
}
|
|
804
1078
|
return defaultValue
|
|
805
1079
|
};
|
|
806
1080
|
|
|
807
|
-
return {
|
|
808
|
-
Provider,
|
|
809
|
-
useContext,
|
|
810
|
-
}
|
|
1081
|
+
return { Provider, useContext }
|
|
811
1082
|
};
|
|
812
1083
|
|
|
813
1084
|
const useQuery = () => {
|
|
1085
|
+
if (typeof window === 'undefined') return {}
|
|
1086
|
+
|
|
814
1087
|
const searchParams = new URLSearchParams(window.location.search);
|
|
815
1088
|
const query = {};
|
|
816
|
-
for (
|
|
1089
|
+
for (const [key, value] of searchParams.entries()) {
|
|
817
1090
|
query[key] = value;
|
|
818
1091
|
}
|
|
819
1092
|
return query
|
|
820
1093
|
};
|
|
821
1094
|
|
|
822
1095
|
const useHash = () => {
|
|
1096
|
+
if (typeof window === 'undefined') return ''
|
|
1097
|
+
|
|
823
1098
|
const [hash, setHash] = useStore(window.location.hash);
|
|
824
1099
|
useEffect(() => {
|
|
825
|
-
const onHashChange = () =>
|
|
826
|
-
setHash(window.location.hash);
|
|
827
|
-
};
|
|
1100
|
+
const onHashChange = () => setHash(window.location.hash);
|
|
828
1101
|
window.addEventListener('hashchange', onHashChange);
|
|
829
1102
|
return () => window.removeEventListener('hashchange', onHashChange)
|
|
830
1103
|
}, []);
|
|
831
1104
|
return hash
|
|
832
1105
|
};
|
|
833
1106
|
|
|
1107
|
+
const useMetadata = (tags = {}, options = {}) => {
|
|
1108
|
+
useEffect(() => {
|
|
1109
|
+
if (typeof document === 'undefined') return
|
|
1110
|
+
|
|
1111
|
+
let finalTitle = 'Ryunix App';
|
|
1112
|
+
const template = options.title?.template;
|
|
1113
|
+
const defaultTitle = options.title?.prefix || 'Ryunix App';
|
|
1114
|
+
const pageTitle = tags.pageTitle || tags.title;
|
|
1115
|
+
|
|
1116
|
+
if (is.string(pageTitle) && pageTitle.trim()) {
|
|
1117
|
+
finalTitle = template?.includes('%s')
|
|
1118
|
+
? template.replace('%s', pageTitle)
|
|
1119
|
+
: pageTitle;
|
|
1120
|
+
} else {
|
|
1121
|
+
finalTitle = defaultTitle;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
document.title = finalTitle;
|
|
1125
|
+
|
|
1126
|
+
if (tags.canonical) {
|
|
1127
|
+
let link = document.querySelector('link[rel="canonical"]');
|
|
1128
|
+
if (!link) {
|
|
1129
|
+
link = document.createElement('link');
|
|
1130
|
+
link.setAttribute('rel', 'canonical');
|
|
1131
|
+
document.head.appendChild(link);
|
|
1132
|
+
}
|
|
1133
|
+
link.setAttribute('href', tags.canonical);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
Object.entries(tags).forEach(([key, value]) => {
|
|
1137
|
+
if (['title', 'pageTitle', 'canonical'].includes(key)) return
|
|
1138
|
+
|
|
1139
|
+
const isProperty = key.startsWith('og:') || key.startsWith('twitter:');
|
|
1140
|
+
const selector = `meta[${isProperty ? 'property' : 'name'}='${key}']`;
|
|
1141
|
+
let meta = document.head.querySelector(selector);
|
|
1142
|
+
|
|
1143
|
+
if (!meta) {
|
|
1144
|
+
meta = document.createElement('meta');
|
|
1145
|
+
meta.setAttribute(isProperty ? 'property' : 'name', key);
|
|
1146
|
+
document.head.appendChild(meta);
|
|
1147
|
+
}
|
|
1148
|
+
meta.setAttribute('content', value);
|
|
1149
|
+
});
|
|
1150
|
+
}, [JSON.stringify(tags), JSON.stringify(options)]);
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
// Router Context
|
|
834
1154
|
const RouterContext = createContext('ryunix.navigation', {
|
|
835
1155
|
location: '/',
|
|
836
1156
|
params: {},
|
|
@@ -841,7 +1161,6 @@
|
|
|
841
1161
|
|
|
842
1162
|
const findRoute = (routes, path) => {
|
|
843
1163
|
const pathname = path.split('?')[0].split('#')[0];
|
|
844
|
-
|
|
845
1164
|
const notFoundRoute = routes.find((route) => route.NotFound);
|
|
846
1165
|
const notFound = notFoundRoute
|
|
847
1166
|
? { route: { component: notFoundRoute.NotFound }, params: {} }
|
|
@@ -852,18 +1171,8 @@
|
|
|
852
1171
|
const childRoute = findRoute(route.subRoutes, path);
|
|
853
1172
|
if (childRoute) return childRoute
|
|
854
1173
|
}
|
|
855
|
-
|
|
856
|
-
if (route.path
|
|
857
|
-
return notFound
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
if (!route.path || typeof route.path !== 'string') {
|
|
861
|
-
console.warn('Invalid route detected:', route);
|
|
862
|
-
console.info(
|
|
863
|
-
"if you are using { NotFound: NotFound } please add { path: '*', NotFound: NotFound }",
|
|
864
|
-
);
|
|
865
|
-
continue
|
|
866
|
-
}
|
|
1174
|
+
if (route.path === '*') return notFound
|
|
1175
|
+
if (!route.path || typeof route.path !== 'string') continue
|
|
867
1176
|
|
|
868
1177
|
const keys = [];
|
|
869
1178
|
const pattern = new RegExp(
|
|
@@ -879,11 +1188,9 @@
|
|
|
879
1188
|
acc[key] = match[index + 1];
|
|
880
1189
|
return acc
|
|
881
1190
|
}, {});
|
|
882
|
-
|
|
883
1191
|
return { route, params }
|
|
884
1192
|
}
|
|
885
1193
|
}
|
|
886
|
-
|
|
887
1194
|
return notFound
|
|
888
1195
|
};
|
|
889
1196
|
|
|
@@ -892,7 +1199,6 @@
|
|
|
892
1199
|
|
|
893
1200
|
useEffect(() => {
|
|
894
1201
|
const update = () => setLocation(window.location.pathname);
|
|
895
|
-
|
|
896
1202
|
window.addEventListener('popstate', update);
|
|
897
1203
|
window.addEventListener('hashchange', update);
|
|
898
1204
|
return () => {
|
|
@@ -907,7 +1213,6 @@
|
|
|
907
1213
|
};
|
|
908
1214
|
|
|
909
1215
|
const currentRouteData = findRoute(routes, location) || {};
|
|
910
|
-
|
|
911
1216
|
const query = useQuery();
|
|
912
1217
|
|
|
913
1218
|
const contextValue = {
|
|
@@ -921,9 +1226,7 @@
|
|
|
921
1226
|
return createElement(
|
|
922
1227
|
RouterContext.Provider,
|
|
923
1228
|
{ value: contextValue },
|
|
924
|
-
Fragment({
|
|
925
|
-
children: children,
|
|
926
|
-
}),
|
|
1229
|
+
Fragment({ children }),
|
|
927
1230
|
)
|
|
928
1231
|
};
|
|
929
1232
|
|
|
@@ -954,7 +1257,6 @@
|
|
|
954
1257
|
|
|
955
1258
|
const NavLink = ({ to, exact = false, ...props }) => {
|
|
956
1259
|
const { location, navigate } = useRouter();
|
|
957
|
-
|
|
958
1260
|
const isActive = exact ? location === to : location.startsWith(to);
|
|
959
1261
|
|
|
960
1262
|
const resolveClass = (cls) =>
|
|
@@ -966,7 +1268,6 @@
|
|
|
966
1268
|
};
|
|
967
1269
|
|
|
968
1270
|
const classAttrName = props['ryunix-class'] ? 'ryunix-class' : 'className';
|
|
969
|
-
|
|
970
1271
|
const classAttrValue = resolveClass(
|
|
971
1272
|
props['ryunix-class'] || props['className'],
|
|
972
1273
|
);
|
|
@@ -990,74 +1291,59 @@
|
|
|
990
1291
|
};
|
|
991
1292
|
|
|
992
1293
|
/**
|
|
993
|
-
*
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1294
|
+
* useStore with priority support
|
|
1295
|
+
*/
|
|
1296
|
+
const useStorePriority = (initialState) => {
|
|
1297
|
+
const reducer = (state, action) =>
|
|
1298
|
+
typeof action === 'function' ? action.value(state) : action.value;
|
|
1299
|
+
|
|
1300
|
+
const [state, baseDispatch] = useReducer(reducer, initialState);
|
|
1301
|
+
|
|
1302
|
+
const dispatch = (action, priority = Priority.NORMAL) => {
|
|
1303
|
+
const wrappedAction = {
|
|
1304
|
+
value: action,
|
|
1305
|
+
priority,
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
baseDispatch(wrappedAction);
|
|
1309
|
+
};
|
|
1310
|
+
|
|
1311
|
+
return [state, dispatch]
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* useTransition - Mark updates as non-urgent
|
|
1316
|
+
*/
|
|
1317
|
+
const useTransition = () => {
|
|
1318
|
+
const [isPending, setIsPending] = useStorePriority(false);
|
|
1319
|
+
|
|
1320
|
+
const startTransition = (callback) => {
|
|
1321
|
+
setIsPending(true, Priority.IMMEDIATE);
|
|
997
1322
|
|
|
1323
|
+
setTimeout(() => {
|
|
1324
|
+
callback();
|
|
1325
|
+
setIsPending(false, Priority.IMMEDIATE);
|
|
1326
|
+
}, 0);
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
return [isPending, startTransition]
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* useDeferredValue - Defer value updates
|
|
998
1334
|
*/
|
|
1335
|
+
const useDeferredValue = (value) => {
|
|
1336
|
+
const [deferredValue, setDeferredValue] = useStorePriority(value);
|
|
999
1337
|
|
|
1000
|
-
const useMetadata = (tags = {}, options = {}) => {
|
|
1001
1338
|
useEffect(() => {
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
let template = undefined;
|
|
1006
|
-
let defaultTitle = 'Ryunix App';
|
|
1007
|
-
if (options.title && typeof options.title === 'object') {
|
|
1008
|
-
template = options.title.template;
|
|
1009
|
-
if (typeof options.title.prefix === 'string') {
|
|
1010
|
-
defaultTitle = options.title.prefix;
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1339
|
+
const timeout = setTimeout(() => {
|
|
1340
|
+
setDeferredValue(value, Priority.LOW);
|
|
1341
|
+
}, 100);
|
|
1013
1342
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1343
|
+
return () => clearTimeout(timeout)
|
|
1344
|
+
}, [value]);
|
|
1016
1345
|
|
|
1017
|
-
|
|
1018
|
-
if (pageTitle.trim() === '') {
|
|
1019
|
-
finalTitle = defaultTitle;
|
|
1020
|
-
} else if (template && template.includes('%s')) {
|
|
1021
|
-
finalTitle = template.replace('%s', pageTitle);
|
|
1022
|
-
} else {
|
|
1023
|
-
finalTitle = pageTitle;
|
|
1024
|
-
}
|
|
1025
|
-
} else if (typeof pageTitle === 'object' && pageTitle !== null) {
|
|
1026
|
-
finalTitle = defaultTitle;
|
|
1027
|
-
} else if (!pageTitle) {
|
|
1028
|
-
finalTitle = defaultTitle;
|
|
1029
|
-
}
|
|
1030
|
-
document.title = finalTitle;
|
|
1031
|
-
// Canonical
|
|
1032
|
-
if (tags.canonical) {
|
|
1033
|
-
let link = document.querySelector('link[rel="canonical"]');
|
|
1034
|
-
if (!link) {
|
|
1035
|
-
link = document.createElement('link');
|
|
1036
|
-
link.setAttribute('rel', 'canonical');
|
|
1037
|
-
document.head.appendChild(link);
|
|
1038
|
-
}
|
|
1039
|
-
link.setAttribute('href', tags.canonical);
|
|
1040
|
-
}
|
|
1041
|
-
// Meta tags
|
|
1042
|
-
Object.entries(tags).forEach(([key, value]) => {
|
|
1043
|
-
if (key === 'title' || key === 'pageTitle' || key === 'canonical') return
|
|
1044
|
-
let selector = `meta[name='${key}']`;
|
|
1045
|
-
if (key.startsWith('og:') || key.startsWith('twitter:')) {
|
|
1046
|
-
selector = `meta[property='${key}']`;
|
|
1047
|
-
}
|
|
1048
|
-
let meta = document.head.querySelector(selector);
|
|
1049
|
-
if (!meta) {
|
|
1050
|
-
meta = document.createElement('meta');
|
|
1051
|
-
if (key.startsWith('og:') || key.startsWith('twitter:')) {
|
|
1052
|
-
meta.setAttribute('property', key);
|
|
1053
|
-
} else {
|
|
1054
|
-
meta.setAttribute('name', key);
|
|
1055
|
-
}
|
|
1056
|
-
document.head.appendChild(meta);
|
|
1057
|
-
}
|
|
1058
|
-
meta.setAttribute('content', value);
|
|
1059
|
-
});
|
|
1060
|
-
}, [JSON.stringify(tags), JSON.stringify(options)]);
|
|
1346
|
+
return deferredValue
|
|
1061
1347
|
};
|
|
1062
1348
|
|
|
1063
1349
|
var Hooks = /*#__PURE__*/Object.freeze({
|
|
@@ -1067,22 +1353,136 @@
|
|
|
1067
1353
|
RouterProvider: RouterProvider,
|
|
1068
1354
|
createContext: createContext,
|
|
1069
1355
|
useCallback: useCallback,
|
|
1356
|
+
useDeferredValue: useDeferredValue,
|
|
1070
1357
|
useEffect: useEffect,
|
|
1071
1358
|
useHash: useHash,
|
|
1072
1359
|
useMemo: useMemo,
|
|
1073
1360
|
useMetadata: useMetadata,
|
|
1074
1361
|
useQuery: useQuery,
|
|
1362
|
+
useReducer: useReducer,
|
|
1075
1363
|
useRef: useRef,
|
|
1076
1364
|
useRouter: useRouter,
|
|
1077
|
-
useStore: useStore
|
|
1365
|
+
useStore: useStore,
|
|
1366
|
+
useStorePriority: useStorePriority,
|
|
1367
|
+
useTransition: useTransition
|
|
1078
1368
|
});
|
|
1079
1369
|
|
|
1370
|
+
/**
|
|
1371
|
+
* memo - Memoize component to prevent unnecessary re-renders
|
|
1372
|
+
*/
|
|
1373
|
+
const memo = (Component, arePropsEqual) => {
|
|
1374
|
+
return (props) => {
|
|
1375
|
+
const memoizedElement = useMemo(() => {
|
|
1376
|
+
return Component(props)
|
|
1377
|
+
}, [
|
|
1378
|
+
// Default comparison: shallow props comparison
|
|
1379
|
+
...Object.values(props),
|
|
1380
|
+
]);
|
|
1381
|
+
|
|
1382
|
+
return memoizedElement
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* Lazy load component
|
|
1388
|
+
*/
|
|
1389
|
+
const lazy = (importFn) => {
|
|
1390
|
+
let Component = null;
|
|
1391
|
+
let promise = null;
|
|
1392
|
+
let error = null;
|
|
1393
|
+
|
|
1394
|
+
return (props) => {
|
|
1395
|
+
const [, forceUpdate] = useStore(0);
|
|
1396
|
+
|
|
1397
|
+
useEffect(() => {
|
|
1398
|
+
if (Component || error) return
|
|
1399
|
+
|
|
1400
|
+
if (!promise) {
|
|
1401
|
+
promise = importFn()
|
|
1402
|
+
.then((module) => {
|
|
1403
|
+
Component = module.default || module;
|
|
1404
|
+
forceUpdate((x) => x + 1);
|
|
1405
|
+
})
|
|
1406
|
+
.catch((err) => {
|
|
1407
|
+
error = err;
|
|
1408
|
+
forceUpdate((x) => x + 1);
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
}, []);
|
|
1412
|
+
|
|
1413
|
+
if (error) throw error
|
|
1414
|
+
if (!Component) return null
|
|
1415
|
+
return createElement(Component, props)
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Suspense component (basic implementation)
|
|
1421
|
+
*/
|
|
1422
|
+
const Suspense = ({ fallback, children }) => {
|
|
1423
|
+
const [isLoading, setIsLoading] = useStore(true);
|
|
1424
|
+
|
|
1425
|
+
useEffect(() => {
|
|
1426
|
+
setIsLoading(false);
|
|
1427
|
+
}, []);
|
|
1428
|
+
|
|
1429
|
+
if (isLoading && fallback) {
|
|
1430
|
+
return fallback
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
return children
|
|
1434
|
+
};
|
|
1435
|
+
|
|
1436
|
+
let isBatching = false;
|
|
1437
|
+
let pendingUpdates = [];
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* Batch multiple state updates into single render
|
|
1441
|
+
*/
|
|
1442
|
+
const batchUpdates = (callback) => {
|
|
1443
|
+
const wasBatching = isBatching;
|
|
1444
|
+
isBatching = true;
|
|
1445
|
+
|
|
1446
|
+
try {
|
|
1447
|
+
callback();
|
|
1448
|
+
} finally {
|
|
1449
|
+
isBatching = wasBatching;
|
|
1450
|
+
|
|
1451
|
+
if (!isBatching && pendingUpdates.length > 0) {
|
|
1452
|
+
flushUpdates();
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Flush all pending updates
|
|
1459
|
+
*/
|
|
1460
|
+
const flushUpdates = () => {
|
|
1461
|
+
if (pendingUpdates.length === 0) return
|
|
1462
|
+
|
|
1463
|
+
const updates = pendingUpdates;
|
|
1464
|
+
pendingUpdates = [];
|
|
1465
|
+
|
|
1466
|
+
// Execute all updates
|
|
1467
|
+
updates.forEach((update) => update());
|
|
1468
|
+
};
|
|
1469
|
+
|
|
1470
|
+
// Ryunix.*
|
|
1080
1471
|
var Ryunix = {
|
|
1081
1472
|
createElement,
|
|
1082
1473
|
render,
|
|
1083
1474
|
init,
|
|
1084
1475
|
Fragment,
|
|
1085
1476
|
Hooks,
|
|
1477
|
+
memo,
|
|
1478
|
+
lazy,
|
|
1479
|
+
Suspense,
|
|
1480
|
+
safeRender,
|
|
1481
|
+
batchUpdates,
|
|
1482
|
+
Priority,
|
|
1483
|
+
profiler,
|
|
1484
|
+
useProfiler,
|
|
1485
|
+
withProfiler,
|
|
1086
1486
|
};
|
|
1087
1487
|
|
|
1088
1488
|
window.Ryunix = Ryunix;
|
|
@@ -1094,14 +1494,18 @@
|
|
|
1094
1494
|
exports.createContext = createContext;
|
|
1095
1495
|
exports.default = Ryunix;
|
|
1096
1496
|
exports.useCallback = useCallback;
|
|
1497
|
+
exports.useDeferredValue = useDeferredValue;
|
|
1097
1498
|
exports.useEffect = useEffect;
|
|
1098
1499
|
exports.useHash = useHash;
|
|
1099
1500
|
exports.useMemo = useMemo;
|
|
1100
1501
|
exports.useMetadata = useMetadata;
|
|
1101
1502
|
exports.useQuery = useQuery;
|
|
1503
|
+
exports.useReducer = useReducer;
|
|
1102
1504
|
exports.useRef = useRef;
|
|
1103
1505
|
exports.useRouter = useRouter;
|
|
1104
1506
|
exports.useStore = useStore;
|
|
1507
|
+
exports.useStorePriority = useStorePriority;
|
|
1508
|
+
exports.useTransition = useTransition;
|
|
1105
1509
|
|
|
1106
1510
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
1107
1511
|
|