vaderjs 2.2.5 → 2.3.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/README.MD +61 -39
- package/cli.ts +186 -0
- package/index.ts +838 -718
- package/jsconfig.json +2 -2
- package/main.js +414 -600
- package/package.json +1 -1
- package/plugins/index.ts +63 -0
- package/plugins/tailwind.ts +2 -0
- package/README.md +0 -89
- package/bundler/index.js +0 -295
- package/document/index.ts +0 -77
- package/examples/counter/index.jsx +0 -10
package/index.ts
CHANGED
|
@@ -1,805 +1,925 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file A lightweight React-like library with hooks implementation.
|
|
3
|
+
* @module vader
|
|
4
|
+
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @description - The params object is used to store the parameters of the current URL
|
|
25
|
-
* @example
|
|
26
|
-
* // URL: https://example.com?name=John
|
|
27
|
-
* console.log(params.name) // John
|
|
28
|
-
* @example
|
|
29
|
-
* // URL: https://example.com/:name/:age
|
|
30
|
-
* // GO: https://example.com/John/20
|
|
31
|
-
* console.log(params.name) // John
|
|
32
|
-
* console.log(params.age) // 20
|
|
33
|
-
*/
|
|
34
|
-
let params: { [key: string]: string };
|
|
35
|
-
let localStorage: []
|
|
36
|
-
}
|
|
37
|
-
//@ts-ignore
|
|
38
|
-
globalThis.isServer = typeof window === "undefined";
|
|
39
|
-
//@ts-ignore
|
|
40
|
-
if(isServer){
|
|
41
|
-
globalThis.params = {
|
|
42
|
-
[Symbol.iterator]: function* () {
|
|
43
|
-
for (const key in this) {
|
|
44
|
-
yield [key, this[key]];
|
|
45
|
-
}
|
|
46
|
-
},
|
|
6
|
+
/**
|
|
7
|
+
* Global variables for the fiber tree and rendering process.
|
|
8
|
+
*/
|
|
9
|
+
let nextUnitOfWork: Fiber | null = null;
|
|
10
|
+
let wipRoot: Fiber | null = null;
|
|
11
|
+
let currentRoot: Fiber | null = null;
|
|
12
|
+
let deletions: Fiber[] | null = null;
|
|
13
|
+
let wipFiber: Fiber | null = null;
|
|
14
|
+
let hookIndex = 0;
|
|
15
|
+
let isRenderScheduled = false;
|
|
16
|
+
|
|
17
|
+
interface Fiber {
|
|
18
|
+
type?: string | Function;
|
|
19
|
+
dom?: Node;
|
|
20
|
+
props: {
|
|
21
|
+
children: VNode[];
|
|
22
|
+
[key: string]: any;
|
|
47
23
|
};
|
|
24
|
+
parent?: Fiber;
|
|
25
|
+
child?: Fiber;
|
|
26
|
+
sibling?: Fiber;
|
|
27
|
+
alternate?: Fiber;
|
|
28
|
+
effectTag?: "PLACEMENT" | "UPDATE" | "DELETION";
|
|
29
|
+
hooks?: Hook[];
|
|
30
|
+
key?: string | number | null;
|
|
48
31
|
}
|
|
49
32
|
|
|
33
|
+
interface VNode {
|
|
34
|
+
type: string | Function;
|
|
35
|
+
props: {
|
|
36
|
+
children: VNode[];
|
|
37
|
+
[key: string]: any;
|
|
38
|
+
};
|
|
39
|
+
key?: string | number | null;
|
|
40
|
+
}
|
|
50
41
|
|
|
42
|
+
interface Hook {
|
|
43
|
+
state?: any;
|
|
44
|
+
queue?: any[];
|
|
45
|
+
deps?: any[];
|
|
46
|
+
_cleanupFn?: Function;
|
|
47
|
+
memoizedValue?: any;
|
|
48
|
+
current?: any;
|
|
49
|
+
}
|
|
51
50
|
|
|
52
51
|
/**
|
|
53
|
-
*
|
|
54
|
-
* @param
|
|
55
|
-
* @
|
|
56
|
-
* @returns [data, loading, error]
|
|
52
|
+
* Checks if a property key is an event handler.
|
|
53
|
+
* @param {string} key - The property key to check.
|
|
54
|
+
* @returns {boolean} True if the key is an event handler.
|
|
57
55
|
*/
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
};
|
|
56
|
+
const isEvent = (key: string) => key.startsWith("on");
|
|
57
|
+
|
|
61
58
|
/**
|
|
62
|
-
*
|
|
63
|
-
* @param
|
|
64
|
-
* @
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* }
|
|
72
|
-
*
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
59
|
+
* Checks if a property key is a regular property (not children or event).
|
|
60
|
+
* @param {string} key - The property key to check.
|
|
61
|
+
* @returns {boolean} True if the key is a regular property.
|
|
62
|
+
*/
|
|
63
|
+
const isProperty = (key: string) => key !== "children" && !isEvent(key);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates a function to check if a property has changed between objects.
|
|
67
|
+
* @param {object} prev - The previous object.
|
|
68
|
+
* @param {object} next - The next object.
|
|
69
|
+
* @returns {function} A function that takes a key and returns true if the property changed.
|
|
70
|
+
*/
|
|
71
|
+
const isNew = (prev: object, next: object) => (key: string) => prev[key] !== next[key];
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Creates a function to check if a property was removed from an object.
|
|
75
|
+
* @param {object} prev - The previous object.
|
|
76
|
+
* @param {object} next - The next object.
|
|
77
|
+
* @returns {function} A function that takes a key and returns true if the property was removed.
|
|
78
|
+
*/
|
|
79
|
+
const isGone = (prev: object, next: object) => (key: string) => !(key in next);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Creates a DOM node for a fiber.
|
|
83
|
+
* @param {Fiber} fiber - The fiber to create a DOM node for.
|
|
84
|
+
* @returns {Node} The created DOM node.
|
|
85
|
+
*/
|
|
86
|
+
function createDom(fiber: Fiber): Node {
|
|
87
|
+
const dom =
|
|
88
|
+
fiber.type == "TEXT_ELEMENT"
|
|
89
|
+
? document.createTextNode("")
|
|
90
|
+
: document.createElement(fiber.type as string);
|
|
91
|
+
|
|
92
|
+
updateDom(dom, {}, fiber.props);
|
|
93
|
+
return dom;
|
|
90
94
|
}
|
|
95
|
+
|
|
91
96
|
/**
|
|
92
|
-
*
|
|
93
|
-
* @param
|
|
94
|
-
* @
|
|
95
|
-
* @
|
|
96
|
-
* const inputRef = useRef();
|
|
97
|
-
* <input ref={inputRef} />
|
|
98
|
-
* console.log(inputRef.current) // <input />
|
|
97
|
+
* Applies updated props to a DOM node.
|
|
98
|
+
* @param {Node} dom - The DOM node to update.
|
|
99
|
+
* @param {object} prevProps - The previous properties.
|
|
100
|
+
* @param {object} nextProps - The new properties.
|
|
99
101
|
*/
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
function updateDom(dom: Node, prevProps: object, nextProps: object): void {
|
|
103
|
+
prevProps = prevProps || {};
|
|
104
|
+
nextProps = nextProps || {};
|
|
105
|
+
|
|
106
|
+
// Remove old or changed event listeners
|
|
107
|
+
Object.keys(prevProps)
|
|
108
|
+
.filter(isEvent)
|
|
109
|
+
.filter((key) => !(key in nextProps) || isNew(prevProps, nextProps)(key))
|
|
110
|
+
.forEach((name) => {
|
|
111
|
+
const eventType = name.toLowerCase().substring(2);
|
|
112
|
+
if (typeof prevProps[name] === 'function') {
|
|
113
|
+
dom.removeEventListener(eventType, prevProps[name]);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Remove old properties
|
|
118
|
+
Object.keys(prevProps)
|
|
119
|
+
.filter(isProperty)
|
|
120
|
+
.filter(isGone(prevProps, nextProps))
|
|
121
|
+
.forEach((name) => {
|
|
122
|
+
// FIX: Handle both `class` and `className`
|
|
123
|
+
if (name === 'className' || name === 'class') {
|
|
124
|
+
(dom as HTMLElement).removeAttribute("class");
|
|
125
|
+
} else {
|
|
126
|
+
dom[name] = "";
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Set new or changed properties
|
|
131
|
+
Object.keys(nextProps)
|
|
132
|
+
.filter(isProperty)
|
|
133
|
+
.filter(isNew(prevProps, nextProps))
|
|
134
|
+
.forEach((name) => {
|
|
135
|
+
if (name === 'style' && typeof nextProps[name] === 'string') {
|
|
136
|
+
(dom as HTMLElement).style.cssText = nextProps[name];
|
|
137
|
+
} else if (name === 'className' || name === 'class') {
|
|
138
|
+
// FIX: Handle both `class` and `className`
|
|
139
|
+
(dom as HTMLElement).className = nextProps[name];
|
|
140
|
+
} else {
|
|
141
|
+
dom[name] = nextProps[name];
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Add new event listeners
|
|
146
|
+
Object.keys(nextProps)
|
|
147
|
+
.filter(isEvent)
|
|
148
|
+
.filter(isNew(prevProps, nextProps))
|
|
149
|
+
.forEach((name) => {
|
|
150
|
+
const eventType = name.toLowerCase().substring(2);
|
|
151
|
+
const handler = nextProps[name];
|
|
152
|
+
if (typeof handler === 'function') {
|
|
153
|
+
dom.addEventListener(eventType, handler);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
102
156
|
}
|
|
103
157
|
|
|
104
158
|
/**
|
|
105
|
-
*
|
|
106
|
-
* @param promise
|
|
107
|
-
* @returns
|
|
159
|
+
* Commits the entire work-in-progress tree to the DOM.
|
|
108
160
|
*/
|
|
109
|
-
|
|
110
|
-
|
|
161
|
+
function commitRoot(): void {
|
|
162
|
+
deletions.forEach(commitWork);
|
|
163
|
+
commitWork(wipRoot.child);
|
|
164
|
+
currentRoot = wipRoot;
|
|
165
|
+
wipRoot = null;
|
|
166
|
+
isRenderScheduled = false;
|
|
111
167
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Recursively commits a fiber and its children to the DOM.
|
|
171
|
+
* @param {Fiber} fiber - The fiber to commit.
|
|
172
|
+
*/
|
|
173
|
+
function commitWork(fiber: Fiber | null): void {
|
|
174
|
+
if (!fiber) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let domParentFiber = fiber.parent;
|
|
179
|
+
while (domParentFiber && !domParentFiber.dom) {
|
|
180
|
+
domParentFiber = domParentFiber.parent;
|
|
181
|
+
}
|
|
182
|
+
const domParent = domParentFiber ? domParentFiber.dom : null;
|
|
183
|
+
|
|
184
|
+
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
|
|
185
|
+
if (domParent) domParent.appendChild(fiber.dom);
|
|
186
|
+
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
|
|
187
|
+
updateDom(fiber.dom, fiber.alternate?.props ?? {}, fiber.props);
|
|
188
|
+
} else if (fiber.effectTag === "DELETION") {
|
|
189
|
+
commitDeletion(fiber);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
commitWork(fiber.child);
|
|
193
|
+
commitWork(fiber.sibling);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Recursively removes a fiber and its children from the DOM.
|
|
198
|
+
* @param {Fiber} fiber - The fiber to remove.
|
|
199
|
+
*/
|
|
200
|
+
function commitDeletion(fiber: Fiber | null): void {
|
|
201
|
+
if (!fiber) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (fiber.dom) {
|
|
205
|
+
if (fiber.dom.parentNode) {
|
|
206
|
+
fiber.dom.parentNode.removeChild(fiber.dom);
|
|
207
|
+
}
|
|
208
|
+
} else if (fiber.child) {
|
|
209
|
+
commitDeletion(fiber.child);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Renders a virtual DOM element into a container.
|
|
215
|
+
* @param {VNode} element - The root virtual DOM element to render.
|
|
216
|
+
* @param {Node} container - The DOM container to render into.
|
|
217
|
+
*/
|
|
218
|
+
export function render(element: VNode, container: Node): void {
|
|
219
|
+
container.innerHTML = "";
|
|
220
|
+
|
|
221
|
+
wipRoot = {
|
|
222
|
+
dom: container,
|
|
223
|
+
props: {
|
|
224
|
+
children: [element],
|
|
225
|
+
},
|
|
226
|
+
alternate: currentRoot,
|
|
227
|
+
};
|
|
228
|
+
deletions = [];
|
|
229
|
+
nextUnitOfWork = wipRoot;
|
|
230
|
+
|
|
231
|
+
if (!isRenderScheduled) {
|
|
232
|
+
isRenderScheduled = true;
|
|
233
|
+
requestAnimationFrame(workLoop);
|
|
116
234
|
}
|
|
117
235
|
}
|
|
118
236
|
|
|
119
|
-
|
|
237
|
+
/**
|
|
238
|
+
* The main work loop for rendering and reconciliation.
|
|
239
|
+
*/
|
|
240
|
+
function workLoop(): void {
|
|
241
|
+
if (!wipRoot && currentRoot) {
|
|
242
|
+
wipRoot = {
|
|
243
|
+
dom: currentRoot.dom,
|
|
244
|
+
props: currentRoot.props,
|
|
245
|
+
alternate: currentRoot,
|
|
246
|
+
};
|
|
247
|
+
deletions = [];
|
|
248
|
+
nextUnitOfWork = wipRoot;
|
|
249
|
+
}
|
|
120
250
|
|
|
251
|
+
while (nextUnitOfWork) {
|
|
252
|
+
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
|
|
253
|
+
}
|
|
121
254
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
* @description Set the elements classlist
|
|
125
|
-
*/
|
|
126
|
-
className?: string;
|
|
127
|
-
/**
|
|
128
|
-
* @description Once clicked send user to a different link
|
|
129
|
-
*/
|
|
130
|
-
href?: string;
|
|
131
|
-
style?: string;
|
|
132
|
-
openInNewTab?: boolean
|
|
133
|
-
onClick?: () => void;
|
|
134
|
-
onChange?: () => void;
|
|
135
|
-
}, children: any) => {
|
|
136
|
-
function handleClick(e) {
|
|
137
|
-
e.preventDefault();
|
|
138
|
-
if (props.openInNewTab) {
|
|
139
|
-
window.open(props.href, "_blank");
|
|
140
|
-
return void 0;
|
|
141
|
-
}
|
|
142
|
-
window.history.pushState({}, "", props.href);
|
|
143
|
-
window.dispatchEvent(new Event("popstate"));
|
|
144
|
-
window.dispatchEvent(new Event("load"));
|
|
145
|
-
window.location.reload();
|
|
146
|
-
return void 0;
|
|
255
|
+
if (!nextUnitOfWork && wipRoot) {
|
|
256
|
+
commitRoot();
|
|
147
257
|
}
|
|
148
|
-
return e("a", { ...props, onClick: handleClick }, props.children);
|
|
149
258
|
}
|
|
150
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Performs work on a single fiber unit.
|
|
262
|
+
* @param {Fiber} fiber - The fiber to perform work on.
|
|
263
|
+
* @returns {Fiber|null} The next fiber to work on.
|
|
264
|
+
*/
|
|
265
|
+
function performUnitOfWork(fiber: Fiber): Fiber | null {
|
|
266
|
+
const isFunctionComponent = fiber.type instanceof Function;
|
|
151
267
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if(typeof window !== "undefined") {
|
|
161
|
-
window.history.back = () => {
|
|
162
|
-
window.history.go(-1);
|
|
163
|
-
}
|
|
164
|
-
window.history.forward = () => {
|
|
165
|
-
window.history.go(1);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
globalThis.Fragment = Fragment;
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* @description - Create a new element
|
|
172
|
-
* @param element
|
|
173
|
-
* @param props
|
|
174
|
-
* @param children
|
|
175
|
-
* @returns
|
|
176
|
-
*/
|
|
177
|
-
export const e = (element, props, ...children) => {
|
|
178
|
-
if (!element)
|
|
179
|
-
return "";
|
|
180
|
-
let instance;
|
|
181
|
-
switch (true) {
|
|
182
|
-
case isClassComponent(element):
|
|
183
|
-
instance = new element;
|
|
184
|
-
instance.props = props;
|
|
185
|
-
instance.children = children;
|
|
186
|
-
instance.Mounted = true;
|
|
187
|
-
return instance.render(props);
|
|
188
|
-
case typeof element === "function":
|
|
189
|
-
instance = memoizeClassComponent(Component, element.name);
|
|
190
|
-
element = element.bind(instance);
|
|
191
|
-
instance.render = (props) => element(props);
|
|
192
|
-
if (element.name.toLowerCase() == "default") {
|
|
193
|
-
throw new Error("Function name must be unique");
|
|
194
|
-
}
|
|
195
|
-
instance.key = element.name;
|
|
196
|
-
instance.Mounted = true;
|
|
197
|
-
let firstEl = instance.render({ key: instance.key, children, ...props }, children);
|
|
198
|
-
instance.children = children;
|
|
199
|
-
if (!firstEl)
|
|
200
|
-
firstEl = { type: "div", props: { key: instance.key, ...props }, children };
|
|
201
|
-
firstEl.props = { key: instance.key, ...firstEl.props, ...props };
|
|
202
|
-
firstEl.props["idKey"] = instance.props?.ref?.key || instance.key;
|
|
203
|
-
instance.props = firstEl.props;
|
|
204
|
-
return firstEl;
|
|
205
|
-
default:
|
|
206
|
-
if(!element) {
|
|
207
|
-
return "";
|
|
208
|
-
}
|
|
209
|
-
let el = { type: element, props: props || {}, children: children || [] };
|
|
210
|
-
if (el.type !== "head") {
|
|
211
|
-
el.props = { idKey: el.props?.ref?.key || crypto.randomUUID(), ...el.props };
|
|
212
|
-
}
|
|
268
|
+
if (isFunctionComponent) {
|
|
269
|
+
updateFunctionComponent(fiber);
|
|
270
|
+
} else {
|
|
271
|
+
updateHostComponent(fiber);
|
|
272
|
+
}
|
|
213
273
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
274
|
+
if (fiber.child) {
|
|
275
|
+
return fiber.child;
|
|
276
|
+
}
|
|
218
277
|
|
|
219
|
-
|
|
278
|
+
let nextFiber = fiber;
|
|
279
|
+
while (nextFiber) {
|
|
280
|
+
if (nextFiber.sibling) {
|
|
281
|
+
return nextFiber.sibling;
|
|
282
|
+
}
|
|
283
|
+
nextFiber = nextFiber.parent;
|
|
220
284
|
}
|
|
221
|
-
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
222
287
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Updates a function component fiber.
|
|
290
|
+
* @param {Fiber} fiber - The function component fiber to update.
|
|
291
|
+
*/
|
|
292
|
+
function updateFunctionComponent(fiber: Fiber): void {
|
|
293
|
+
wipFiber = fiber;
|
|
294
|
+
hookIndex = 0;
|
|
295
|
+
wipFiber.hooks = fiber.alternate ? fiber.alternate.hooks : [];
|
|
230
296
|
|
|
297
|
+
const children = [fiber.type(fiber.props)]
|
|
298
|
+
.flat()
|
|
299
|
+
.filter(child => child != null && typeof child !== 'boolean')
|
|
300
|
+
.map(child => typeof child === "object" ? child : createTextElement(child));
|
|
231
301
|
|
|
232
|
-
|
|
233
|
-
children: any[] | any;
|
|
302
|
+
reconcileChildren(fiber, children);
|
|
234
303
|
}
|
|
235
304
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Updates a host component fiber (DOM element).
|
|
307
|
+
* @param {Fiber} fiber - The host component fiber to update.
|
|
308
|
+
*/
|
|
309
|
+
function updateHostComponent(fiber: Fiber): void {
|
|
310
|
+
if (!fiber.dom) {
|
|
311
|
+
fiber.dom = createDom(fiber);
|
|
312
|
+
}
|
|
313
|
+
reconcileChildren(fiber, fiber.props.children);
|
|
314
|
+
}
|
|
240
315
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
316
|
+
/**
|
|
317
|
+
* Reconciles the children of a fiber with new elements.
|
|
318
|
+
* @param {Fiber} wipFiber - The work-in-progress fiber.
|
|
319
|
+
* @param {VNode[]} elements - The new child elements.
|
|
320
|
+
*/
|
|
321
|
+
function reconcileChildren(wipFiber: Fiber, elements: VNode[]): void {
|
|
322
|
+
let index = 0;
|
|
323
|
+
let oldFiber = wipFiber.alternate?.child;
|
|
324
|
+
let prevSibling = null;
|
|
325
|
+
|
|
326
|
+
const oldFibersByKey = new Map<string | number, Fiber>();
|
|
327
|
+
|
|
328
|
+
while (oldFiber) {
|
|
329
|
+
const key = oldFiber.key != null ? oldFiber.key : index;
|
|
330
|
+
oldFibersByKey.set(key, oldFiber);
|
|
331
|
+
oldFiber = oldFiber.sibling;
|
|
332
|
+
index++;
|
|
333
|
+
}
|
|
244
334
|
|
|
335
|
+
index = 0;
|
|
336
|
+
prevSibling = null;
|
|
337
|
+
|
|
338
|
+
for (; index < elements.length; index++) {
|
|
339
|
+
const element = elements[index];
|
|
340
|
+
const key = element.key != null ? element.key : index;
|
|
341
|
+
const oldFiber = oldFibersByKey.get(key);
|
|
342
|
+
const sameType = oldFiber && element.type === oldFiber.type;
|
|
343
|
+
|
|
344
|
+
let newFiber: Fiber | null = null;
|
|
345
|
+
|
|
346
|
+
if (sameType) {
|
|
347
|
+
newFiber = {
|
|
348
|
+
type: oldFiber.type,
|
|
349
|
+
props: element.props,
|
|
350
|
+
dom: oldFiber.dom,
|
|
351
|
+
parent: wipFiber,
|
|
352
|
+
alternate: oldFiber,
|
|
353
|
+
effectTag: "UPDATE",
|
|
354
|
+
key,
|
|
355
|
+
};
|
|
356
|
+
oldFibersByKey.delete(key);
|
|
357
|
+
} else {
|
|
358
|
+
if (element) {
|
|
359
|
+
newFiber = {
|
|
360
|
+
type: element.type,
|
|
361
|
+
props: element.props,
|
|
362
|
+
dom: null,
|
|
363
|
+
parent: wipFiber,
|
|
364
|
+
alternate: null,
|
|
365
|
+
effectTag: "PLACEMENT",
|
|
366
|
+
key,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
245
370
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return { type: "div", props: {
|
|
251
|
-
idKey: crypto.randomUUID()
|
|
252
|
-
}, children: [child] };
|
|
371
|
+
if (prevSibling == null) {
|
|
372
|
+
wipFiber.child = newFiber;
|
|
373
|
+
} else {
|
|
374
|
+
prevSibling.sibling = newFiber;
|
|
253
375
|
}
|
|
376
|
+
prevSibling = newFiber;
|
|
254
377
|
}
|
|
255
|
-
|
|
378
|
+
|
|
379
|
+
oldFibersByKey.forEach(fiber => {
|
|
380
|
+
fiber.effectTag = "DELETION";
|
|
381
|
+
deletions.push(fiber);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
if (prevSibling) prevSibling.sibling = null;
|
|
256
385
|
}
|
|
257
386
|
|
|
258
387
|
/**
|
|
259
|
-
*
|
|
260
|
-
* @param
|
|
261
|
-
* @
|
|
388
|
+
* Creates a virtual DOM element.
|
|
389
|
+
* @param {string|Function} type - The type of the element.
|
|
390
|
+
* @param {object} props - The element's properties.
|
|
391
|
+
* @param {...any} children - The element's children.
|
|
392
|
+
* @returns {VNode} The created virtual DOM element.
|
|
262
393
|
*/
|
|
263
|
-
export function
|
|
264
|
-
|
|
394
|
+
export function createElement(
|
|
395
|
+
type: string | Function,
|
|
396
|
+
props?: object,
|
|
397
|
+
...children: any[]
|
|
398
|
+
): VNode {
|
|
399
|
+
return {
|
|
400
|
+
type,
|
|
401
|
+
props: {
|
|
402
|
+
...props,
|
|
403
|
+
children: children
|
|
404
|
+
.flat()
|
|
405
|
+
.filter(child => child != null && typeof child !== "boolean")
|
|
406
|
+
.map(child =>
|
|
407
|
+
typeof child === "object" ? child : createTextElement(child)
|
|
408
|
+
),
|
|
409
|
+
},
|
|
410
|
+
key: props?.key ?? null,
|
|
411
|
+
};
|
|
265
412
|
}
|
|
413
|
+
|
|
266
414
|
/**
|
|
267
|
-
*
|
|
268
|
-
* @param
|
|
269
|
-
* @
|
|
270
|
-
* @param persist - persist state on reload
|
|
271
|
-
* @returns {T, (newState: any, Element: string) => void, key}
|
|
415
|
+
* Creates a text virtual DOM element.
|
|
416
|
+
* @param {string} text - The text content.
|
|
417
|
+
* @returns {VNode} The created text element.
|
|
272
418
|
*/
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
419
|
+
function createTextElement(text: string): VNode {
|
|
420
|
+
return {
|
|
421
|
+
type: "TEXT_ELEMENT",
|
|
422
|
+
props: {
|
|
423
|
+
nodeValue: text,
|
|
424
|
+
children: [],
|
|
425
|
+
},
|
|
277
426
|
};
|
|
278
|
-
|
|
279
|
-
return [initialState, setState];
|
|
280
|
-
};
|
|
427
|
+
}
|
|
281
428
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
* const App = (props) => {
|
|
295
|
-
* return (
|
|
296
|
-
* <div>
|
|
297
|
-
* <h1>Hello, {props.name}</h1>
|
|
298
|
-
* </div>
|
|
299
|
-
* )
|
|
300
|
-
* }
|
|
301
|
-
*
|
|
302
|
-
* render(<App name="John" />, document.getElementById("root"));
|
|
303
|
-
*/
|
|
304
|
-
|
|
305
|
-
// create a hidden object on window
|
|
306
|
-
//
|
|
307
|
-
if (!isServer) {
|
|
308
|
-
Object.defineProperty(window, "state", {
|
|
309
|
-
value: [],
|
|
310
|
-
writable: true,
|
|
311
|
-
enumerable: true,
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
} else {
|
|
315
|
-
globalThis.state = []
|
|
316
|
-
}
|
|
317
|
-
export class Component {
|
|
318
|
-
props;
|
|
319
|
-
state;
|
|
320
|
-
element;
|
|
321
|
-
Mounted;
|
|
322
|
-
effect;
|
|
323
|
-
key;
|
|
324
|
-
effectCalls: any[]
|
|
325
|
-
eventRegistry: any
|
|
326
|
-
prevState;
|
|
327
|
-
refs: HTMLElement[] | any[]
|
|
328
|
-
state: {}
|
|
329
|
-
constructor() {
|
|
330
|
-
this.key = crypto.randomUUID();
|
|
331
|
-
this.props = {};
|
|
332
|
-
this.effect = [];
|
|
333
|
-
this.Mounted = false;
|
|
334
|
-
this.state = {};
|
|
335
|
-
this.element = null;
|
|
336
|
-
this.effectCalls = []
|
|
337
|
-
this.errorThreshold = 1000
|
|
338
|
-
this.maxIntervalCalls = 10
|
|
339
|
-
this.eventRegistry = new WeakMap();
|
|
340
|
-
this.refs = []
|
|
341
|
-
}
|
|
342
|
-
useRef = (key, value) => {
|
|
343
|
-
if (!this.refs.find((r) => r.key == key)) {
|
|
344
|
-
this.refs.push({ key, current: value});
|
|
345
|
-
}
|
|
429
|
+
/**
|
|
430
|
+
* A React-like useState hook for managing component state.
|
|
431
|
+
* @template T
|
|
432
|
+
* @param {T|(() => T)} initial - The initial state value or initializer function.
|
|
433
|
+
* @returns {[T, (action: T | ((prevState: T) => T)) => void]} A stateful value and a function to update it.
|
|
434
|
+
*/
|
|
435
|
+
export function useState<T>(
|
|
436
|
+
initial: T | (() => T)
|
|
437
|
+
): [T, (action: T | ((prevState: T) => T)) => void] {
|
|
438
|
+
if (!wipFiber) {
|
|
439
|
+
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
440
|
+
}
|
|
346
441
|
|
|
347
|
-
|
|
442
|
+
let hook = wipFiber.hooks[hookIndex];
|
|
443
|
+
if (!hook) {
|
|
444
|
+
hook = {
|
|
445
|
+
state: typeof initial === "function" ? (initial as () => T)() : initial,
|
|
446
|
+
queue: []
|
|
447
|
+
};
|
|
448
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
348
449
|
}
|
|
349
|
-
useEffect(callback, dependencies = []) {
|
|
350
|
-
const callbackId = callback.toString(); // Unique ID based on callback string representation
|
|
351
450
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
451
|
+
hook.queue.forEach((action) => {
|
|
452
|
+
hook.state = typeof action === "function" ? action(hook.state) : action;
|
|
453
|
+
});
|
|
454
|
+
hook.queue = [];
|
|
455
|
+
|
|
456
|
+
const setState = (action: T | ((prevState: T) => T)) => {
|
|
457
|
+
hook.queue.push(action);
|
|
458
|
+
if (!isRenderScheduled) {
|
|
459
|
+
isRenderScheduled = true;
|
|
460
|
+
requestAnimationFrame(workLoop);
|
|
361
461
|
}
|
|
462
|
+
};
|
|
362
463
|
|
|
363
|
-
|
|
464
|
+
hookIndex++;
|
|
465
|
+
return [hook.state, setState];
|
|
466
|
+
}
|
|
364
467
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
468
|
+
/**
|
|
469
|
+
* A React-like useEffect hook for side effects.
|
|
470
|
+
* @param {Function} callback - The effect callback.
|
|
471
|
+
* @param {Array} deps - The dependency array.
|
|
472
|
+
*/
|
|
473
|
+
export function useEffect(callback: Function, deps?: any[]): void {
|
|
474
|
+
if (!wipFiber) {
|
|
475
|
+
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
let hook = wipFiber.hooks[hookIndex];
|
|
479
|
+
if (!hook) {
|
|
480
|
+
hook = { deps: undefined, _cleanupFn: undefined };
|
|
481
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const hasChanged = hook.deps === undefined ||
|
|
485
|
+
!deps ||
|
|
486
|
+
deps.some((dep, i) => !Object.is(dep, hook.deps[i]));
|
|
380
487
|
|
|
381
|
-
|
|
488
|
+
if (hasChanged) {
|
|
489
|
+
if (hook._cleanupFn) {
|
|
490
|
+
hook._cleanupFn();
|
|
491
|
+
}
|
|
492
|
+
setTimeout(() => {
|
|
493
|
+
const newCleanup = callback();
|
|
494
|
+
if (typeof newCleanup === 'function') {
|
|
495
|
+
hook._cleanupFn = newCleanup;
|
|
496
|
+
} else {
|
|
497
|
+
hook._cleanupFn = undefined;
|
|
498
|
+
}
|
|
499
|
+
}, 0);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
hook.deps = deps;
|
|
503
|
+
hookIndex++;
|
|
504
|
+
}
|
|
382
505
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
506
|
+
/**
|
|
507
|
+
* A switch component for conditional rendering.
|
|
508
|
+
* @param {object} props - The component props.
|
|
509
|
+
* @param {VNode[]} props.children - The child components.
|
|
510
|
+
* @returns {VNode|null} The matched child or null.
|
|
511
|
+
*/
|
|
512
|
+
export function Switch({ children }: { children: VNode[] }): VNode | null {
|
|
513
|
+
const childrenArray = Array.isArray(children) ? children : [children];
|
|
514
|
+
const match = childrenArray.find(child => child && child.props.when);
|
|
515
|
+
if (match) {
|
|
516
|
+
return match;
|
|
517
|
+
}
|
|
518
|
+
return childrenArray.find(child => child && child.props.default) || null;
|
|
519
|
+
}
|
|
392
520
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
521
|
+
/**
|
|
522
|
+
* A match component for use with Switch.
|
|
523
|
+
* @param {object} props - The component props.
|
|
524
|
+
* @param {boolean} props.when - The condition to match.
|
|
525
|
+
* @param {VNode[]} props.children - The child components.
|
|
526
|
+
* @returns {VNode|null} The children if when is true, otherwise null.
|
|
527
|
+
*/
|
|
528
|
+
export function Match({ when, children }: { when: boolean, children: VNode[] }): VNode | null {
|
|
529
|
+
return when ? children : null;
|
|
530
|
+
}
|
|
400
531
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
532
|
+
/**
|
|
533
|
+
* A React-like useRef hook for mutable references.
|
|
534
|
+
* @template T
|
|
535
|
+
* @param {T} initial - The initial reference value.
|
|
536
|
+
* @returns {{current: T}} A mutable ref object.
|
|
537
|
+
*/
|
|
538
|
+
export function useRef<T>(initial: T): { current: T } {
|
|
539
|
+
if (!wipFiber) {
|
|
540
|
+
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
541
|
+
}
|
|
405
542
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
JSON.stringify(previousDependencies[i]) !== JSON.stringify(dependencies[i])
|
|
412
|
-
) {
|
|
413
|
-
dependenciesChanged = true;
|
|
414
|
-
break;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
543
|
+
let hook = wipFiber.hooks[hookIndex];
|
|
544
|
+
if (!hook) {
|
|
545
|
+
hook = { current: initial };
|
|
546
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
547
|
+
}
|
|
417
548
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
executeCallback();
|
|
421
|
-
effectCall.dependencies = dependencies;
|
|
422
|
-
}
|
|
549
|
+
hookIndex++;
|
|
550
|
+
return hook;
|
|
423
551
|
}
|
|
424
552
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
553
|
+
/**
|
|
554
|
+
* A React-like useLayoutEffect hook that runs synchronously after DOM mutations.
|
|
555
|
+
* @param {Function} callback - The effect callback.
|
|
556
|
+
* @param {Array} deps - The dependency array.
|
|
557
|
+
*/
|
|
558
|
+
export function useLayoutEffect(callback: Function, deps?: any[]): void {
|
|
559
|
+
if (!wipFiber) {
|
|
560
|
+
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
561
|
+
}
|
|
432
562
|
|
|
433
|
-
|
|
434
|
-
|
|
563
|
+
let hook = wipFiber.hooks[hookIndex];
|
|
564
|
+
if (!hook) {
|
|
565
|
+
hook = { deps: undefined, _cleanupFn: undefined };
|
|
566
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const hasChanged = hook.deps === undefined ||
|
|
570
|
+
!deps ||
|
|
571
|
+
deps.some((dep, i) => !Object.is(dep, hook.deps[i]));
|
|
572
|
+
|
|
573
|
+
if (hasChanged) {
|
|
574
|
+
if (hook._cleanupFn) {
|
|
575
|
+
hook._cleanupFn();
|
|
435
576
|
}
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (persist) {
|
|
442
|
-
sessionStorage.setItem(key, JSON.stringify({ value: newValue }));
|
|
443
|
-
}
|
|
444
|
-
this.forceUpdate(this.key);
|
|
445
|
-
};
|
|
446
|
-
return [value, setValue];
|
|
447
|
-
}
|
|
448
|
-
useFetch(url, options) {
|
|
449
|
-
const loadingKey = "loading_" + url;
|
|
450
|
-
const errorKey = "error" + url;
|
|
451
|
-
const dataKey = "_data" + url;
|
|
452
|
-
let [loading, setLoading, _clear1] = this.useState(loadingKey, true);
|
|
453
|
-
let [error, setError, _clear2] = this.useState(errorKey, null);
|
|
454
|
-
let [data, setData, clear] = this.useState(dataKey, null);
|
|
455
|
-
if (loading() && !error() && !data()) {
|
|
456
|
-
fetch(url, options).then((res) => res.json()).then((data2) => {
|
|
457
|
-
setLoading(false);
|
|
458
|
-
setData(data2);
|
|
459
|
-
this.forceUpdate(this.key);
|
|
460
|
-
setTimeout(() => {
|
|
461
|
-
_clear1()
|
|
462
|
-
_clear2()
|
|
463
|
-
clear()
|
|
464
|
-
}, 1500)
|
|
465
|
-
}).catch((err) => {
|
|
466
|
-
setError(err);
|
|
467
|
-
this.forceUpdate(this.key);
|
|
468
|
-
});
|
|
577
|
+
const cleanup = callback();
|
|
578
|
+
if (typeof cleanup === 'function') {
|
|
579
|
+
hook._cleanupFn = cleanup;
|
|
580
|
+
} else {
|
|
581
|
+
hook._cleanupFn = undefined;
|
|
469
582
|
}
|
|
470
|
-
return { loading, error, data };
|
|
471
583
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
584
|
+
|
|
585
|
+
hook.deps = deps;
|
|
586
|
+
hookIndex++;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* A React-like useReducer hook for state management with reducers.
|
|
591
|
+
* @template S
|
|
592
|
+
* @template A
|
|
593
|
+
* @param {(state: S, action: A) => S} reducer - The reducer function.
|
|
594
|
+
* @param {S} initialState - The initial state.
|
|
595
|
+
* @returns {[S, (action: A) => void]} The current state and dispatch function.
|
|
596
|
+
*/
|
|
597
|
+
export function useReducer<S, A>(
|
|
598
|
+
reducer: (state: S, action: A) => S,
|
|
599
|
+
initialState: S
|
|
600
|
+
): [S, (action: A) => void] {
|
|
601
|
+
if (!wipFiber) {
|
|
602
|
+
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
let hook = wipFiber.hooks[hookIndex];
|
|
606
|
+
if (!hook) {
|
|
607
|
+
hook = {
|
|
608
|
+
state: initialState,
|
|
609
|
+
queue: [],
|
|
610
|
+
};
|
|
611
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
hook.queue.forEach((action) => {
|
|
615
|
+
hook.state = reducer(hook.state, action);
|
|
616
|
+
});
|
|
617
|
+
hook.queue = [];
|
|
618
|
+
|
|
619
|
+
const dispatch = (action: A) => {
|
|
620
|
+
hook.queue.push(action);
|
|
621
|
+
if (!isRenderScheduled) {
|
|
622
|
+
isRenderScheduled = true;
|
|
623
|
+
requestAnimationFrame(workLoop);
|
|
475
624
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
hookIndex++;
|
|
628
|
+
return [hook.state, dispatch];
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* A React-like useContext hook for accessing context values.
|
|
633
|
+
* @template T
|
|
634
|
+
* @param {Context<T>} Context - The context object to use.
|
|
635
|
+
* @returns {T} The current context value.
|
|
636
|
+
*/
|
|
637
|
+
export function useContext<T>(Context: Context<T>): T {
|
|
638
|
+
if (!wipFiber) {
|
|
639
|
+
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
let fiber = wipFiber.parent;
|
|
643
|
+
while (fiber) {
|
|
644
|
+
if (fiber.type && fiber.type._context === Context) {
|
|
645
|
+
return fiber.props.value;
|
|
482
646
|
}
|
|
647
|
+
fiber = fiber.parent;
|
|
483
648
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
// Traverse children recursively
|
|
508
|
-
const children = Array.from(source.childNodes || []);
|
|
509
|
-
const elementChildren = Array.from(element.childNodes || []);
|
|
510
|
-
|
|
511
|
-
children.forEach((child, index) => {
|
|
512
|
-
if (elementChildren[index]) {
|
|
513
|
-
this.attachEventsRecursively(elementChildren[index], child);
|
|
514
|
-
}
|
|
515
|
-
});
|
|
649
|
+
|
|
650
|
+
return Context._defaultValue;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
interface Context<T> {
|
|
654
|
+
_defaultValue: T;
|
|
655
|
+
Provider: Function & { _context: Context<T> };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Creates a context object for use with useContext.
|
|
660
|
+
* @template T
|
|
661
|
+
* @param {T} defaultValue - The default context value.
|
|
662
|
+
* @returns {Context<T>} The created context object.
|
|
663
|
+
*/
|
|
664
|
+
export function createContext<T>(defaultValue: T): Context<T> {
|
|
665
|
+
const context = {
|
|
666
|
+
_defaultValue: defaultValue,
|
|
667
|
+
Provider: function Provider({ children }: { children: VNode[] }) {
|
|
668
|
+
return children;
|
|
669
|
+
},
|
|
516
670
|
};
|
|
671
|
+
context.Provider._context = context;
|
|
672
|
+
return context;
|
|
673
|
+
}
|
|
517
674
|
|
|
675
|
+
/**
|
|
676
|
+
* A React-like useMemo hook for memoizing expensive calculations.
|
|
677
|
+
* @template T
|
|
678
|
+
* @param {() => T} factory - The function to memoize.
|
|
679
|
+
* @param {Array} deps - The dependency array.
|
|
680
|
+
* @returns {T} The memoized value.
|
|
681
|
+
*/
|
|
682
|
+
export function useMemo<T>(factory: () => T, deps?: any[]): T {
|
|
683
|
+
if (!wipFiber) {
|
|
684
|
+
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
685
|
+
}
|
|
518
686
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (this.Reconciler.shouldUpdate(oldElement, newElement)) {
|
|
525
|
-
// Update attributes
|
|
526
|
-
const oldChildren = Array.from(oldElement.childNodes);
|
|
527
|
-
const newChildren = Array.from(newElement.childNodes);
|
|
528
|
-
|
|
529
|
-
const maxLength = Math.max(oldChildren.length, newChildren.length);
|
|
530
|
-
if (oldElement.tagName !== newElement.tagName) {
|
|
531
|
-
const newElementClone = newElement.cloneNode(true);
|
|
532
|
-
oldElement.replaceWith(newElementClone);
|
|
533
|
-
|
|
534
|
-
// Attach events recursively to the new element
|
|
535
|
-
this.attachEventsRecursively(newElementClone, newElement);
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
687
|
+
let hook = wipFiber.hooks[hookIndex];
|
|
688
|
+
if (!hook) {
|
|
689
|
+
hook = { memoizedValue: factory(), deps };
|
|
690
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
691
|
+
}
|
|
538
692
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
693
|
+
const hasChanged = hook.deps === undefined ||
|
|
694
|
+
!deps ||
|
|
695
|
+
deps.some((dep, i) => !Object.is(dep, hook.deps[i]));
|
|
696
|
+
if (hasChanged) {
|
|
697
|
+
hook.memoizedValue = factory();
|
|
698
|
+
hook.deps = deps;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
hookIndex++;
|
|
702
|
+
return hook.memoizedValue;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* A React-like useCallback hook for memoizing functions.
|
|
707
|
+
* @template T
|
|
708
|
+
* @param {T} callback - The function to memoize.
|
|
709
|
+
* @param {Array} deps - The dependency array.
|
|
710
|
+
* @returns {T} The memoized callback.
|
|
711
|
+
*/
|
|
712
|
+
export function useCallback<T extends Function>(callback: T, deps?: any[]): T {
|
|
713
|
+
return useMemo(() => callback, deps);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* A hook for managing arrays with common operations.
|
|
718
|
+
* @template T
|
|
719
|
+
* @param {T[]} initialValue - The initial array value.
|
|
720
|
+
* @returns {{
|
|
721
|
+
* array: T[],
|
|
722
|
+
* add: (item: T) => void,
|
|
723
|
+
* remove: (index: number) => void,
|
|
724
|
+
* update: (index: number, item: T) => void
|
|
725
|
+
* }} An object with the array and mutation functions.
|
|
726
|
+
*/
|
|
727
|
+
export function useArray<T>(initialValue: T[] = []): {
|
|
728
|
+
array: T[],
|
|
729
|
+
add: (item: T) => void,
|
|
730
|
+
remove: (index: number) => void,
|
|
731
|
+
update: (index: number, item: T) => void
|
|
732
|
+
} {
|
|
733
|
+
const [array, setArray] = useState(initialValue);
|
|
734
|
+
|
|
735
|
+
const add = (item: T) => {
|
|
736
|
+
setArray((prevArray) => [...prevArray, item]);
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
const remove = (index: number) => {
|
|
740
|
+
setArray((prevArray) => prevArray.filter((_, i) => i !== index));
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
const update = (index: number, item: T) => {
|
|
744
|
+
setArray((prevArray) => prevArray.map((prevItem, i) => (i === index ? item : prevItem)));
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
return { array, add, remove, update };
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* A hook for running a function at a fixed interval.
|
|
752
|
+
* @param {Function} callback - The function to run.
|
|
753
|
+
* @param {number|null} delay - The delay in milliseconds, or null to stop.
|
|
754
|
+
*/
|
|
755
|
+
export function useInterval(callback: Function, delay: number | null): void {
|
|
756
|
+
useEffect(() => {
|
|
757
|
+
if (delay === null) return;
|
|
758
|
+
const interval = setInterval(callback, delay);
|
|
759
|
+
return () => clearInterval(interval);
|
|
760
|
+
}, [callback, delay]);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Types for cache configuration
|
|
764
|
+
interface QueryCacheOptions {
|
|
765
|
+
expiryMs?: number; // Cache duration in milliseconds
|
|
766
|
+
enabled?: boolean; // Whether caching is enabled
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Default cache options
|
|
770
|
+
const DEFAULT_CACHE_OPTIONS: QueryCacheOptions = {
|
|
771
|
+
expiryMs: 5 * 60 * 1000, // 5 minutes default
|
|
772
|
+
enabled: true
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
// In-memory cache store
|
|
776
|
+
const queryCache = new Map<string, {
|
|
777
|
+
data: any;
|
|
778
|
+
timestamp: number;
|
|
779
|
+
options: QueryCacheOptions;
|
|
780
|
+
}>();
|
|
781
|
+
|
|
782
|
+
export function useQuery<T>(
|
|
783
|
+
url: string,
|
|
784
|
+
cacheOptions: QueryCacheOptions = {} // Default to empty object
|
|
785
|
+
): {
|
|
786
|
+
data: T | null;
|
|
787
|
+
loading: boolean;
|
|
788
|
+
error: Error | null;
|
|
789
|
+
refetch: () => Promise<void>;
|
|
790
|
+
} {
|
|
791
|
+
const [data, setData] = useState<T | null>(null);
|
|
792
|
+
const [loading, setLoading] = useState(true);
|
|
793
|
+
const [error, setError] = useState<Error | null>(null);
|
|
794
|
+
|
|
795
|
+
// FIX: Destructure primitive values from cacheOptions for stable dependencies.
|
|
796
|
+
const {
|
|
797
|
+
enabled = DEFAULT_CACHE_OPTIONS.enabled,
|
|
798
|
+
expiryMs = DEFAULT_CACHE_OPTIONS.expiryMs
|
|
799
|
+
} = cacheOptions;
|
|
800
|
+
|
|
801
|
+
// FIX: Memoize the options object so its reference is stable across renders.
|
|
802
|
+
// It will only be recreated if `enabled` or `expiryMs` changes.
|
|
803
|
+
const mergedCacheOptions = useMemo(() => ({
|
|
804
|
+
enabled,
|
|
805
|
+
expiryMs,
|
|
806
|
+
}), [enabled, expiryMs]);
|
|
807
|
+
|
|
808
|
+
const fetchData = useCallback(async () => {
|
|
809
|
+
setLoading(true);
|
|
810
|
+
try {
|
|
811
|
+
// Check cache first if enabled
|
|
812
|
+
if (mergedCacheOptions.enabled) {
|
|
813
|
+
const cached = queryCache.get(url);
|
|
814
|
+
const now = Date.now();
|
|
815
|
+
|
|
816
|
+
if (cached && now - cached.timestamp < mergedCacheOptions.expiryMs) {
|
|
817
|
+
setData(cached.data);
|
|
818
|
+
setLoading(false);
|
|
585
819
|
return;
|
|
586
820
|
}
|
|
587
821
|
}
|
|
588
|
-
|
|
589
|
-
// Process children recursively
|
|
590
|
-
const oldChildren = Array.from(oldElement.childNodes);
|
|
591
|
-
const newChildren = Array.from(newElement.childNodes);
|
|
592
|
-
|
|
593
|
-
const maxLength = Math.max(oldChildren.length, newChildren.length);
|
|
594
|
-
|
|
595
|
-
for (let i = 0; i < maxLength; i++) {
|
|
596
|
-
if (i >= oldChildren.length) {
|
|
597
|
-
// Add new child if it exists in newChildren but not in oldChildren
|
|
598
|
-
const newChildClone = newChildren[i].cloneNode(true);
|
|
599
|
-
oldElement.appendChild(newChildClone);
|
|
600
|
-
|
|
601
|
-
// Attach any event listeners
|
|
602
|
-
const newChildEvents = this.eventRegistry.get(newChildren[i]) || [];
|
|
603
|
-
newChildEvents.forEach(({ type, handler }) => {
|
|
604
|
-
this.addEventListener(newChildClone, type, handler);
|
|
605
|
-
});
|
|
606
|
-
} else if (i >= newChildren.length) {
|
|
607
|
-
// Remove child if it exists in oldChildren but not in newChildren
|
|
608
|
-
oldElement.removeChild(oldChildren[i]);
|
|
609
|
-
} else {
|
|
610
|
-
this.Reconciler.update(oldChildren[i], newChildren[i]);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// Reapply events for the current element
|
|
615
|
-
const parentEvents = this.eventRegistry.get(newElement) || [];
|
|
616
|
-
parentEvents.forEach(({ type, handler }) => {
|
|
617
|
-
if (newElement.nodeType === oldElement.nodeType) {
|
|
618
|
-
this.addEventListener(oldElement, type, handler);
|
|
619
|
-
}
|
|
620
|
-
});
|
|
621
822
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
if (
|
|
626
|
-
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// Check if text content differs
|
|
630
|
-
if (oldElement.nodeType === Node.TEXT_NODE) {
|
|
631
|
-
return oldElement.textContent !== newElement.textContent;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// Check if node names differ
|
|
635
|
-
if (oldElement.nodeName !== newElement.nodeName) {
|
|
636
|
-
return true;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// Check if child counts differ
|
|
640
|
-
if (oldElement.childNodes.length !== newElement.childNodes.length) {
|
|
641
|
-
return true;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// Check if attributes differ
|
|
645
|
-
const newAttributes = Array.from(newElement.attributes || []);
|
|
646
|
-
for (let { name, value } of newAttributes) {
|
|
647
|
-
if (oldElement.getAttribute(name) !== value) {
|
|
648
|
-
return true;
|
|
649
|
-
}
|
|
823
|
+
// Not in cache or expired - fetch fresh data
|
|
824
|
+
const response = await fetch(url);
|
|
825
|
+
|
|
826
|
+
if (!response.ok) {
|
|
827
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
650
828
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
let isSvg = svgTags.includes(element.type);
|
|
662
|
-
|
|
663
|
-
// Create the element, using proper namespace for SVG
|
|
664
|
-
let el = isSvg
|
|
665
|
-
? document.createElementNS("http://www.w3.org/2000/svg", element.type)
|
|
666
|
-
: document.createElement(element.type);
|
|
667
|
-
|
|
668
|
-
// Handle text nodes
|
|
669
|
-
if (typeof element === "string" || typeof element === "number" || typeof element === "boolean") {
|
|
670
|
-
el.textContent = element; // Safer alternative to innerHTML
|
|
671
|
-
return el;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// Set attributes
|
|
675
|
-
let attributes = element.props || {};
|
|
676
|
-
for (let key in attributes) {
|
|
677
|
-
if(typeof attributes[key] !== "string" && !acceptedAttributes.includes(key) || !acceptedAttributes.includes(key)) continue;
|
|
678
|
-
if(key === "ref") {
|
|
679
|
-
let _key = attributes[key].key;
|
|
680
|
-
// update the ref
|
|
681
|
-
let ref = this.refs.find((r) => r.key == _key);
|
|
682
|
-
if(ref) {
|
|
683
|
-
ref.current = document.querySelector(`[idKey="${_key}"]`) || el;
|
|
684
|
-
}
|
|
685
|
-
el.setAttribute("idKey", _key);
|
|
686
|
-
element.props.idKey = _key
|
|
687
|
-
}
|
|
688
|
-
else if (key === "key") {
|
|
689
|
-
el.key = attributes[key];
|
|
690
|
-
} else if (key === "className") {
|
|
691
|
-
el.setAttribute("class", attributes[key]);
|
|
692
|
-
} else if (key === "style") {
|
|
693
|
-
let styleObject = attributes[key];
|
|
694
|
-
if (typeof styleObject === "object") {
|
|
695
|
-
for (let styleKey in styleObject) {
|
|
696
|
-
el.style[styleKey] = styleObject[styleKey];
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
} else if (key.startsWith("on")) {
|
|
700
|
-
// Event listeners
|
|
701
|
-
const eventType = key.substring(2).toLowerCase();
|
|
702
|
-
const handler = attributes[key];
|
|
703
|
-
this.eventRegistry.set(el, [...(this.eventRegistry.get(el) || []), { event: eventType, handler }]);
|
|
704
|
-
this.addEventListener(el, eventType, handler);
|
|
705
|
-
} else if (attributes[key] !== null && attributes[key] !== undefined &&
|
|
706
|
-
!key.includes(" ") || !key.includes("-") || !key.includes("_")) {
|
|
707
|
-
try {
|
|
708
|
-
el.setAttribute(key, attributes[key]);
|
|
709
|
-
} catch (error) {
|
|
710
|
-
|
|
711
|
-
}
|
|
712
|
-
} else if(typeof attributes[key] === "object" && key !== "style"){
|
|
713
|
-
continue;
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
// Handle children
|
|
718
|
-
let children = element.children || [];
|
|
719
|
-
children.forEach((child) => {
|
|
720
|
-
if (Array.isArray(child)) {
|
|
721
|
-
// Recursively process nested arrays
|
|
722
|
-
child.forEach((nestedChild) => el.appendChild(this.parseToElement(nestedChild)));
|
|
723
|
-
} else if (typeof child === "function") {
|
|
724
|
-
// Handle functional components
|
|
725
|
-
let component = memoizeClassComponent(Component, child.name);
|
|
726
|
-
component.Mounted = true;
|
|
727
|
-
component.render = (props) => child(props);
|
|
728
|
-
let componentElement = component.toElement();
|
|
729
|
-
el.appendChild(componentElement);
|
|
730
|
-
} else if (typeof child === "object") {
|
|
731
|
-
// Nested object children
|
|
732
|
-
el.appendChild(this.parseToElement(child));
|
|
733
|
-
} else if (child !== null && child !== undefined && child !== false) {
|
|
734
|
-
// Text nodes
|
|
735
|
-
el.appendChild(document.createTextNode(child));
|
|
829
|
+
|
|
830
|
+
const result = await response.json();
|
|
831
|
+
|
|
832
|
+
// Update cache if enabled
|
|
833
|
+
if (mergedCacheOptions.enabled) {
|
|
834
|
+
queryCache.set(url, {
|
|
835
|
+
data: result,
|
|
836
|
+
timestamp: Date.now(),
|
|
837
|
+
options: mergedCacheOptions
|
|
838
|
+
});
|
|
736
839
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
return element();
|
|
840
|
+
|
|
841
|
+
setData(result);
|
|
842
|
+
} catch (err) {
|
|
843
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
844
|
+
} finally {
|
|
845
|
+
setLoading(false);
|
|
744
846
|
}
|
|
745
|
-
|
|
746
|
-
}
|
|
747
|
-
toElement() {
|
|
748
|
-
let children = this.render(this.props);
|
|
749
|
-
let el = this.parseToElement(children);
|
|
750
|
-
el.setAttribute("idKey", this.key);
|
|
751
|
-
return el;
|
|
752
|
-
}
|
|
753
|
-
render() {
|
|
754
|
-
return "";
|
|
755
|
-
}
|
|
756
|
-
}
|
|
847
|
+
}, [url, mergedCacheOptions]); // This dependency is now stable
|
|
757
848
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
}
|
|
764
|
-
return instance;
|
|
849
|
+
useEffect(() => {
|
|
850
|
+
fetchData();
|
|
851
|
+
}, [fetchData]); // This dependency is now stable
|
|
852
|
+
|
|
853
|
+
return { data, loading, error, refetch: fetchData };
|
|
765
854
|
}
|
|
855
|
+
|
|
766
856
|
/**
|
|
767
|
-
*
|
|
768
|
-
* @
|
|
769
|
-
* @param container
|
|
857
|
+
* A hook for tracking window focus state.
|
|
858
|
+
* @returns {boolean} True if the window is focused.
|
|
770
859
|
*/
|
|
771
|
-
export function
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (key.startsWith("state_")) {
|
|
778
|
-
sessionStorage.removeItem(key);
|
|
779
|
-
}
|
|
780
|
-
});
|
|
781
|
-
});
|
|
782
|
-
}
|
|
783
|
-
if (isClassComponent(element)) {
|
|
784
|
-
const instance = new element;
|
|
785
|
-
instance.Mounted = true;
|
|
786
|
-
let el = instance.toElement();
|
|
787
|
-
instance.element = el;
|
|
788
|
-
container.innerHTML = "";
|
|
789
|
-
container.replaceWith(el);
|
|
790
|
-
} else {
|
|
791
|
-
let memoizedInstance = memoizeClassComponent(Component, element.name);
|
|
792
|
-
memoizedInstance.Mounted = true;
|
|
793
|
-
element = element.bind(memoizedInstance);
|
|
860
|
+
export function useWindowFocus(): boolean {
|
|
861
|
+
const [isFocused, setIsFocused] = useState(true);
|
|
862
|
+
|
|
863
|
+
useEffect(() => {
|
|
864
|
+
const onFocus = () => setIsFocused(true);
|
|
865
|
+
const onBlur = () => setIsFocused(false);
|
|
794
866
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
867
|
+
window.addEventListener("focus", onFocus);
|
|
868
|
+
window.addEventListener("blur", onBlur);
|
|
869
|
+
|
|
870
|
+
return () => {
|
|
871
|
+
window.removeEventListener("focus", onFocus);
|
|
872
|
+
window.removeEventListener("blur", onBlur);
|
|
873
|
+
};
|
|
874
|
+
}, []);
|
|
875
|
+
|
|
876
|
+
return isFocused;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* A hook for syncing state with localStorage.
|
|
881
|
+
* @template T
|
|
882
|
+
* @param {string} key - The localStorage key.
|
|
883
|
+
* @param {T} initialValue - The initial value.
|
|
884
|
+
* @returns {[T, (value: T) => void]} The stored value and a function to update it.
|
|
885
|
+
*/
|
|
886
|
+
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
|
|
887
|
+
const [storedValue, setStoredValue] = useState(() => {
|
|
888
|
+
try {
|
|
889
|
+
const item = localStorage.getItem(key);
|
|
890
|
+
return item ? JSON.parse(item) : initialValue;
|
|
891
|
+
} catch (error) {
|
|
892
|
+
return initialValue;
|
|
798
893
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
const setValue = (value: T) => {
|
|
897
|
+
try {
|
|
898
|
+
setStoredValue(value);
|
|
899
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
900
|
+
} catch (error) {
|
|
901
|
+
console.error("Error saving to localStorage", error);
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
return [storedValue, setValue];
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* A hook for detecting clicks outside an element.
|
|
910
|
+
* @param {React.RefObject} ref - A ref to the element to watch.
|
|
911
|
+
* @param {Function} handler - The handler to call when a click outside occurs.
|
|
912
|
+
*/
|
|
913
|
+
export function useOnClickOutside(ref: { current: HTMLElement | null }, handler: Function): void {
|
|
914
|
+
useEffect(() => {
|
|
915
|
+
const listener = (event: MouseEvent) => {
|
|
916
|
+
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
917
|
+
handler(event);
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
document.addEventListener("mousedown", listener);
|
|
921
|
+
return () => {
|
|
922
|
+
document.removeEventListener("mousedown", listener);
|
|
923
|
+
};
|
|
924
|
+
}, [ref, handler]);
|
|
805
925
|
}
|