vaderjs 2.2.6 → 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 +839 -688
- 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/bun.lockb +0 -0
- package/bundler/index.js +0 -295
- package/document/index.ts +0 -77
- package/examples/counter/index.jsx +0 -10
package/index.ts
CHANGED
|
@@ -1,774 +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
|
-
return [null, true, null];
|
|
60
|
-
};
|
|
56
|
+
const isEvent = (key: string) => key.startsWith("on");
|
|
61
57
|
|
|
62
58
|
/**
|
|
63
|
-
*
|
|
64
|
-
* @param
|
|
65
|
-
* @returns {
|
|
66
|
-
* @example
|
|
67
|
-
* const inputRef = useRef();
|
|
68
|
-
* <input ref={inputRef} />
|
|
69
|
-
* console.log(inputRef.current) // <input />
|
|
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.
|
|
70
62
|
*/
|
|
71
|
-
|
|
72
|
-
return { key: crypto.randomUUID(), current: value };
|
|
73
|
-
}
|
|
63
|
+
const isProperty = (key: string) => key !== "children" && !isEvent(key);
|
|
74
64
|
|
|
75
65
|
/**
|
|
76
|
-
*
|
|
77
|
-
* @param
|
|
78
|
-
* @
|
|
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.
|
|
79
70
|
*/
|
|
80
|
-
|
|
81
|
-
return [null, () => { }];
|
|
82
|
-
}
|
|
83
|
-
export const useEffect = (callback: any, dependencies: any[] = []) => {
|
|
84
|
-
dependencies = dependencies.map((dep) => JSON.stringify(dep));
|
|
85
|
-
if (dependencies.length === 0) {
|
|
86
|
-
callback();
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// make a switch function component
|
|
71
|
+
const isNew = (prev: object, next: object) => (key: string) => prev[key] !== next[key];
|
|
91
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);
|
|
92
80
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}, children: any) => {
|
|
107
|
-
function handleClick(e) {
|
|
108
|
-
e.preventDefault();
|
|
109
|
-
if (props.openInNewTab) {
|
|
110
|
-
window.open(props.href, "_blank");
|
|
111
|
-
return void 0;
|
|
112
|
-
}
|
|
113
|
-
window.history.pushState({}, "", props.href);
|
|
114
|
-
window.dispatchEvent(new Event("popstate"));
|
|
115
|
-
window.dispatchEvent(new Event("load"));
|
|
116
|
-
window.location.reload();
|
|
117
|
-
return void 0;
|
|
118
|
-
}
|
|
119
|
-
return e("a", { ...props, onClick: handleClick }, props.children);
|
|
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;
|
|
120
94
|
}
|
|
121
95
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* @description - Create a new element
|
|
143
|
-
* @param element
|
|
144
|
-
* @param props
|
|
145
|
-
* @param children
|
|
146
|
-
* @returns
|
|
147
|
-
*/
|
|
148
|
-
export const e = (element, props, ...children) => {
|
|
149
|
-
if (!element)
|
|
150
|
-
return "";
|
|
151
|
-
let instance;
|
|
152
|
-
switch (true) {
|
|
153
|
-
case isClassComponent(element):
|
|
154
|
-
instance = new element;
|
|
155
|
-
instance.props = props;
|
|
156
|
-
instance.children = children;
|
|
157
|
-
instance.Mounted = true;
|
|
158
|
-
return instance.render(props);
|
|
159
|
-
case typeof element === "function":
|
|
160
|
-
instance = memoizeClassComponent(Component, element.name);
|
|
161
|
-
element = element.bind(instance);
|
|
162
|
-
instance.render = (props) => element(props);
|
|
163
|
-
if (element.name.toLowerCase() == "default") {
|
|
164
|
-
throw new Error("Function name must be unique");
|
|
96
|
+
/**
|
|
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.
|
|
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]);
|
|
165
114
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return "";
|
|
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] = "";
|
|
179
127
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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];
|
|
183
142
|
}
|
|
143
|
+
});
|
|
184
144
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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);
|
|
188
154
|
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
189
157
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Commits the entire work-in-progress tree to the DOM.
|
|
160
|
+
*/
|
|
161
|
+
function commitRoot(): void {
|
|
162
|
+
deletions.forEach(commitWork);
|
|
163
|
+
commitWork(wipRoot.child);
|
|
164
|
+
currentRoot = wipRoot;
|
|
165
|
+
wipRoot = null;
|
|
166
|
+
isRenderScheduled = false;
|
|
167
|
+
}
|
|
193
168
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
+
}
|
|
201
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
|
+
}
|
|
202
191
|
|
|
203
|
-
|
|
204
|
-
|
|
192
|
+
commitWork(fiber.child);
|
|
193
|
+
commitWork(fiber.sibling);
|
|
205
194
|
}
|
|
206
195
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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);
|
|
214
207
|
}
|
|
208
|
+
} else if (fiber.child) {
|
|
209
|
+
commitDeletion(fiber.child);
|
|
215
210
|
}
|
|
216
|
-
return { type: "div", props: {}, children: [] };
|
|
217
211
|
}
|
|
218
212
|
|
|
219
213
|
/**
|
|
220
|
-
*
|
|
221
|
-
* @param
|
|
222
|
-
* @
|
|
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.
|
|
223
217
|
*/
|
|
224
|
-
export function
|
|
225
|
-
|
|
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);
|
|
234
|
+
}
|
|
226
235
|
}
|
|
236
|
+
|
|
227
237
|
/**
|
|
228
|
-
*
|
|
229
|
-
* @param key
|
|
230
|
-
* @param initialState
|
|
231
|
-
* @param persist - persist state on reload
|
|
232
|
-
* @returns {T, (newState: any, Element: string) => void, key}
|
|
238
|
+
* The main work loop for rendering and reconciliation.
|
|
233
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
|
+
}
|
|
234
250
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
return [initialState, setState];
|
|
241
|
-
};
|
|
251
|
+
while (nextUnitOfWork) {
|
|
252
|
+
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
|
|
253
|
+
}
|
|
242
254
|
|
|
243
|
-
if (!
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* @description - Create a new component
|
|
250
|
-
* @param element
|
|
251
|
-
* @param props
|
|
252
|
-
* @param children
|
|
253
|
-
* @returns
|
|
254
|
-
* @example
|
|
255
|
-
* const App = (props) => {
|
|
256
|
-
* return (
|
|
257
|
-
* <div>
|
|
258
|
-
* <h1>Hello, {props.name}</h1>
|
|
259
|
-
* </div>
|
|
260
|
-
* )
|
|
261
|
-
* }
|
|
262
|
-
*
|
|
263
|
-
* render(<App name="John" />, document.getElementById("root"));
|
|
264
|
-
*/
|
|
265
|
-
|
|
266
|
-
// create a hidden object on window
|
|
267
|
-
//
|
|
268
|
-
if (!isServer) {
|
|
269
|
-
Object.defineProperty(window, "state", {
|
|
270
|
-
value: [],
|
|
271
|
-
writable: true,
|
|
272
|
-
enumerable: true,
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
} else {
|
|
276
|
-
globalThis.state = []
|
|
277
|
-
}
|
|
278
|
-
if (!crypto.randomUUID) {
|
|
279
|
-
crypto.randomUUID = function() {
|
|
280
|
-
const url = URL.createObjectURL(new Blob());
|
|
281
|
-
const uuid = url.toString();
|
|
282
|
-
URL.revokeObjectURL(url);
|
|
283
|
-
return uuid.slice(uuid.lastIndexOf('/') + 1);
|
|
284
|
-
};
|
|
255
|
+
if (!nextUnitOfWork && wipRoot) {
|
|
256
|
+
commitRoot();
|
|
257
|
+
}
|
|
285
258
|
}
|
|
286
259
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
effectCalls: any[]
|
|
295
|
-
eventRegistry: any
|
|
296
|
-
prevState;
|
|
297
|
-
refs: HTMLElement[] | any[]
|
|
298
|
-
state: {}
|
|
299
|
-
constructor() {
|
|
300
|
-
this.key = crypto.randomUUID();
|
|
301
|
-
this.props = {};
|
|
302
|
-
this.effect = [];
|
|
303
|
-
this.Mounted = false;
|
|
304
|
-
this.state = {};
|
|
305
|
-
this.element = null;
|
|
306
|
-
this.effectCalls = []
|
|
307
|
-
this.errorThreshold = 1000
|
|
308
|
-
this.maxIntervalCalls = 10
|
|
309
|
-
this.eventRegistry = new WeakMap();
|
|
310
|
-
this.refs = []
|
|
311
|
-
}
|
|
312
|
-
useRef = (key, value) => {
|
|
313
|
-
if (!this.refs.find((r) => r.key == key)) {
|
|
314
|
-
this.refs.push({ key, current: value});
|
|
315
|
-
}
|
|
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;
|
|
316
267
|
|
|
317
|
-
|
|
268
|
+
if (isFunctionComponent) {
|
|
269
|
+
updateFunctionComponent(fiber);
|
|
270
|
+
} else {
|
|
271
|
+
updateHostComponent(fiber);
|
|
318
272
|
}
|
|
319
|
-
useEffect(callback, dependencies = []) {
|
|
320
|
-
const callbackId = callback.toString(); // Unique ID based on callback string representation
|
|
321
273
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
});
|
|
274
|
+
if (fiber.child) {
|
|
275
|
+
return fiber.child;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let nextFiber = fiber;
|
|
279
|
+
while (nextFiber) {
|
|
280
|
+
if (nextFiber.sibling) {
|
|
281
|
+
return nextFiber.sibling;
|
|
331
282
|
}
|
|
283
|
+
nextFiber = nextFiber.parent;
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
332
287
|
|
|
333
|
-
|
|
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 : [];
|
|
334
296
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
// Track call counts and handle potential over-calling issues
|
|
340
|
-
if (timeSinceLastCall < this.errorThreshold) {
|
|
341
|
-
effectCall.count += 1;
|
|
342
|
-
if (effectCall.count > this.maxIntervalCalls) {
|
|
343
|
-
throw new Error(
|
|
344
|
-
`Woah, way too many calls! Ensure you are not over-looping. Adjust maxIntervalCalls and errorThreshold as needed.`
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
} else {
|
|
348
|
-
effectCall.count = 1;
|
|
349
|
-
}
|
|
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));
|
|
350
301
|
|
|
351
|
-
|
|
302
|
+
reconcileChildren(fiber, children);
|
|
303
|
+
}
|
|
352
304
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
+
}
|
|
362
315
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
+
}
|
|
370
334
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
+
}
|
|
374
369
|
}
|
|
375
370
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (
|
|
381
|
-
JSON.stringify(previousDependencies[i]) !== JSON.stringify(dependencies[i])
|
|
382
|
-
) {
|
|
383
|
-
dependenciesChanged = true;
|
|
384
|
-
break;
|
|
385
|
-
}
|
|
371
|
+
if (prevSibling == null) {
|
|
372
|
+
wipFiber.child = newFiber;
|
|
373
|
+
} else {
|
|
374
|
+
prevSibling.sibling = newFiber;
|
|
386
375
|
}
|
|
376
|
+
prevSibling = newFiber;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
oldFibersByKey.forEach(fiber => {
|
|
380
|
+
fiber.effectTag = "DELETION";
|
|
381
|
+
deletions.push(fiber);
|
|
382
|
+
});
|
|
387
383
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
384
|
+
if (prevSibling) prevSibling.sibling = null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
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.
|
|
393
|
+
*/
|
|
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
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Creates a text virtual DOM element.
|
|
416
|
+
* @param {string} text - The text content.
|
|
417
|
+
* @returns {VNode} The created text element.
|
|
418
|
+
*/
|
|
419
|
+
function createTextElement(text: string): VNode {
|
|
420
|
+
return {
|
|
421
|
+
type: "TEXT_ELEMENT",
|
|
422
|
+
props: {
|
|
423
|
+
nodeValue: text,
|
|
424
|
+
children: [],
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
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
|
+
}
|
|
441
|
+
|
|
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;
|
|
449
|
+
}
|
|
450
|
+
|
|
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);
|
|
392
461
|
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
hookIndex++;
|
|
465
|
+
return [hook.state, setState];
|
|
393
466
|
}
|
|
394
467
|
|
|
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
|
+
}
|
|
395
477
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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]));
|
|
402
487
|
|
|
403
|
-
|
|
404
|
-
|
|
488
|
+
if (hasChanged) {
|
|
489
|
+
if (hook._cleanupFn) {
|
|
490
|
+
hook._cleanupFn();
|
|
405
491
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
492
|
+
setTimeout(() => {
|
|
493
|
+
const newCleanup = callback();
|
|
494
|
+
if (typeof newCleanup === 'function') {
|
|
495
|
+
hook._cleanupFn = newCleanup;
|
|
496
|
+
} else {
|
|
497
|
+
hook._cleanupFn = undefined;
|
|
409
498
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
499
|
+
}, 0);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
hook.deps = deps;
|
|
503
|
+
hookIndex++;
|
|
504
|
+
}
|
|
505
|
+
|
|
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
|
+
}
|
|
520
|
+
|
|
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
|
+
}
|
|
531
|
+
|
|
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
|
+
}
|
|
542
|
+
|
|
543
|
+
let hook = wipFiber.hooks[hookIndex];
|
|
544
|
+
if (!hook) {
|
|
545
|
+
hook = { current: initial };
|
|
546
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
hookIndex++;
|
|
550
|
+
return hook;
|
|
551
|
+
}
|
|
552
|
+
|
|
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.");
|
|
441
561
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
562
|
+
|
|
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();
|
|
445
576
|
}
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
this.eventRegistry.set(element, registeredEvents);
|
|
577
|
+
const cleanup = callback();
|
|
578
|
+
if (typeof cleanup === 'function') {
|
|
579
|
+
hook._cleanupFn = cleanup;
|
|
580
|
+
} else {
|
|
581
|
+
hook._cleanupFn = undefined;
|
|
452
582
|
}
|
|
453
583
|
}
|
|
454
|
-
removeEventListeners(element) {
|
|
455
|
-
// Unregister and remove all events for the element
|
|
456
|
-
const registeredEvents = this.eventRegistry.get(element) || [];
|
|
457
|
-
registeredEvents.forEach(({ type, handler }) => {
|
|
458
|
-
element.removeEventListener(type, handler);
|
|
459
|
-
});
|
|
460
|
-
this.eventRegistry.delete(element);
|
|
461
|
-
}
|
|
462
|
-
forceUpdate(key) {
|
|
463
|
-
let el = document.querySelector(`[idKey="${key}"]`);
|
|
464
|
-
let newl = this.toElement(this.props);
|
|
465
|
-
if (newl.getAttribute("idKey") !== key) {
|
|
466
|
-
newl = Array.from(newl.children).filter((el2) => el2.getAttribute("idKey") === key)[0];
|
|
467
|
-
}
|
|
468
|
-
this.Reconciler.update(el, newl);
|
|
469
|
-
}
|
|
470
|
-
attachEventsRecursively = (element, source) => {
|
|
471
|
-
// Rebind events for the current element
|
|
472
|
-
const events = this.eventRegistry.get(source) || [];
|
|
473
|
-
events.forEach(({ event, handler }) => {
|
|
474
|
-
this.addEventListener(element, event, handler);
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
// Traverse children recursively
|
|
478
|
-
const children = Array.from(source.childNodes || []);
|
|
479
|
-
const elementChildren = Array.from(element.childNodes || []);
|
|
480
|
-
|
|
481
|
-
children.forEach((child, index) => {
|
|
482
|
-
if (elementChildren[index]) {
|
|
483
|
-
this.attachEventsRecursively(elementChildren[index], child);
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
};
|
|
487
584
|
|
|
585
|
+
hook.deps = deps;
|
|
586
|
+
hookIndex++;
|
|
587
|
+
}
|
|
488
588
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
// Attach events recursively to the new element
|
|
505
|
-
this.attachEventsRecursively(newElementClone, newElement);
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
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
|
+
}
|
|
508
604
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
// Rebind events to the new child (and its children recursively)
|
|
519
|
-
this.attachEventsRecursively(newChildClone, newChildren[i]);
|
|
520
|
-
} else if (i >= newChildren.length) {
|
|
521
|
-
oldElement.removeChild(oldChildren[i]);
|
|
522
|
-
} else {
|
|
523
|
-
this.Reconciler.update(oldChildren[i], newChildren[i]);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
Array.from(oldElement.attributes || []).forEach(({ name }) => {
|
|
528
|
-
if (!newElement.hasAttribute(name)) {
|
|
529
|
-
oldElement.removeAttribute(name);
|
|
530
|
-
}
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
Array.from(newElement.attributes || []).forEach(({ name, value }) => {
|
|
534
|
-
if (oldElement.getAttribute(name) !== value) {
|
|
535
|
-
oldElement.setAttribute(name, value);
|
|
536
|
-
}
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
// Handle text node updates
|
|
540
|
-
if (oldElement.nodeType === Node.TEXT_NODE) {
|
|
541
|
-
if (oldElement.textContent !== newElement.textContent) {
|
|
542
|
-
oldElement.textContent = newElement.textContent;
|
|
543
|
-
}
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// If the element has a single text node, update text directly
|
|
548
|
-
if (
|
|
549
|
-
oldElement.childNodes.length === 1 &&
|
|
550
|
-
oldElement.firstChild.nodeType === Node.TEXT_NODE
|
|
551
|
-
) {
|
|
552
|
-
if (oldElement.textContent !== newElement.textContent) {
|
|
553
|
-
oldElement.textContent = newElement.textContent;
|
|
554
|
-
}
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Process children recursively
|
|
560
|
-
const oldChildren = Array.from(oldElement.childNodes);
|
|
561
|
-
const newChildren = Array.from(newElement.childNodes);
|
|
562
|
-
|
|
563
|
-
const maxLength = Math.max(oldChildren.length, newChildren.length);
|
|
564
|
-
|
|
565
|
-
for (let i = 0; i < maxLength; i++) {
|
|
566
|
-
if (i >= oldChildren.length) {
|
|
567
|
-
// Add new child if it exists in newChildren but not in oldChildren
|
|
568
|
-
const newChildClone = newChildren[i].cloneNode(true);
|
|
569
|
-
oldElement.appendChild(newChildClone);
|
|
570
|
-
|
|
571
|
-
// Attach any event listeners
|
|
572
|
-
const newChildEvents = this.eventRegistry.get(newChildren[i]) || [];
|
|
573
|
-
newChildEvents.forEach(({ type, handler }) => {
|
|
574
|
-
this.addEventListener(newChildClone, type, handler);
|
|
575
|
-
});
|
|
576
|
-
} else if (i >= newChildren.length) {
|
|
577
|
-
// Remove child if it exists in oldChildren but not in newChildren
|
|
578
|
-
oldElement.removeChild(oldChildren[i]);
|
|
579
|
-
} else {
|
|
580
|
-
this.Reconciler.update(oldChildren[i], newChildren[i]);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Reapply events for the current element
|
|
585
|
-
const parentEvents = this.eventRegistry.get(newElement) || [];
|
|
586
|
-
parentEvents.forEach(({ type, handler }) => {
|
|
587
|
-
if (newElement.nodeType === oldElement.nodeType) {
|
|
588
|
-
this.addEventListener(oldElement, type, handler);
|
|
589
|
-
}
|
|
590
|
-
});
|
|
605
|
+
let hook = wipFiber.hooks[hookIndex];
|
|
606
|
+
if (!hook) {
|
|
607
|
+
hook = {
|
|
608
|
+
state: initialState,
|
|
609
|
+
queue: [],
|
|
610
|
+
};
|
|
611
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
612
|
+
}
|
|
591
613
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Check if node names differ
|
|
605
|
-
if (oldElement.nodeName !== newElement.nodeName) {
|
|
606
|
-
return true;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// Check if child counts differ
|
|
610
|
-
if (oldElement.childNodes.length !== newElement.childNodes.length) {
|
|
611
|
-
return true;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// Check if attributes differ
|
|
615
|
-
const newAttributes = Array.from(newElement.attributes || []);
|
|
616
|
-
for (let { name, value } of newAttributes) {
|
|
617
|
-
if (oldElement.getAttribute(name) !== value) {
|
|
618
|
-
return true;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// If no differences found, no update needed
|
|
623
|
-
return false;
|
|
624
|
-
},
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
parseToElement = (element) => {
|
|
628
|
-
if (!element || element.nodeType) return ""
|
|
629
|
-
|
|
630
|
-
let svgTags = ["svg", "path", "circle", "rect", "line", "polyline", "polygon", "ellipse", "g"];
|
|
631
|
-
let isSvg = svgTags.includes(element.type);
|
|
632
|
-
|
|
633
|
-
// Create the element, using proper namespace for SVG
|
|
634
|
-
let el = isSvg
|
|
635
|
-
? document.createElementNS("http://www.w3.org/2000/svg", element.type)
|
|
636
|
-
: document.createElement(element.type);
|
|
637
|
-
|
|
638
|
-
// Handle text nodes
|
|
639
|
-
if (typeof element === "string" || typeof element === "number" || typeof element === "boolean") {
|
|
640
|
-
el.textContent = element; // Safer alternative to innerHTML
|
|
641
|
-
return el;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// Set attributes
|
|
645
|
-
let attributes = element.props || {};
|
|
646
|
-
for (let key in attributes) {
|
|
647
|
-
if(key === "ref") {
|
|
648
|
-
let _key = attributes[key].key;
|
|
649
|
-
// update the ref
|
|
650
|
-
let ref = this.refs.find((r) => r.key == _key);
|
|
651
|
-
if(ref) {
|
|
652
|
-
ref.current = document.querySelector(`[idKey="${_key}"]`) || el;
|
|
653
|
-
}
|
|
654
|
-
el.setAttribute("idKey", _key);
|
|
655
|
-
element.props.idKey = _key
|
|
656
|
-
}
|
|
657
|
-
else if (key === "key") {
|
|
658
|
-
el.key = attributes[key];
|
|
659
|
-
} else if (key === "className") {
|
|
660
|
-
el.setAttribute("class", attributes[key]);
|
|
661
|
-
} else if (key === "style") {
|
|
662
|
-
let styleObject = attributes[key];
|
|
663
|
-
if (typeof styleObject === "object") {
|
|
664
|
-
for (let styleKey in styleObject) {
|
|
665
|
-
el.style[styleKey] = styleObject[styleKey];
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
} else if (key.startsWith("on")) {
|
|
669
|
-
// Event listeners
|
|
670
|
-
const eventType = key.substring(2).toLowerCase();
|
|
671
|
-
const handler = attributes[key];
|
|
672
|
-
this.eventRegistry.set(el, [...(this.eventRegistry.get(el) || []), { event: eventType, handler }]);
|
|
673
|
-
this.addEventListener(el, eventType, handler);
|
|
674
|
-
} else if (attributes[key] !== null && attributes[key] !== undefined &&
|
|
675
|
-
!key.includes(" ") || !key.includes("-") || !key.includes("_")) {
|
|
676
|
-
try {
|
|
677
|
-
el.setAttribute(key, attributes[key]);
|
|
678
|
-
} catch (error) {
|
|
679
|
-
|
|
680
|
-
}
|
|
681
|
-
} else if(typeof attributes[key] === "object" && key !== "style"){
|
|
682
|
-
continue;
|
|
683
|
-
}
|
|
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);
|
|
684
624
|
}
|
|
685
|
-
|
|
686
|
-
// Handle children
|
|
687
|
-
let children = element.children || [];
|
|
688
|
-
children.forEach((child) => {
|
|
689
|
-
if (Array.isArray(child)) {
|
|
690
|
-
// Recursively process nested arrays
|
|
691
|
-
child.forEach((nestedChild) => el.appendChild(this.parseToElement(nestedChild)));
|
|
692
|
-
} else if (typeof child === "function") {
|
|
693
|
-
// Handle functional components
|
|
694
|
-
let component = memoizeClassComponent(Component, child.name);
|
|
695
|
-
component.Mounted = true;
|
|
696
|
-
component.render = (props) => child(props);
|
|
697
|
-
let componentElement = component.toElement();
|
|
698
|
-
el.appendChild(componentElement);
|
|
699
|
-
} else if (typeof child === "object") {
|
|
700
|
-
// Nested object children
|
|
701
|
-
el.appendChild(this.parseToElement(child));
|
|
702
|
-
} else if (child !== null && child !== undefined && child !== false) {
|
|
703
|
-
// Text nodes
|
|
704
|
-
el.appendChild(document.createTextNode(child));
|
|
705
|
-
}
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
return el;
|
|
709
625
|
};
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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;
|
|
713
646
|
}
|
|
714
|
-
|
|
647
|
+
fiber = fiber.parent;
|
|
715
648
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
+
},
|
|
670
|
+
};
|
|
671
|
+
context.Provider._context = context;
|
|
672
|
+
return context;
|
|
673
|
+
}
|
|
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
|
+
}
|
|
686
|
+
|
|
687
|
+
let hook = wipFiber.hooks[hookIndex];
|
|
688
|
+
if (!hook) {
|
|
689
|
+
hook = { memoizedValue: factory(), deps };
|
|
690
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
721
691
|
}
|
|
722
|
-
|
|
723
|
-
|
|
692
|
+
|
|
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;
|
|
724
699
|
}
|
|
700
|
+
|
|
701
|
+
hookIndex++;
|
|
702
|
+
return hook.memoizedValue;
|
|
725
703
|
}
|
|
726
704
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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 };
|
|
734
748
|
}
|
|
749
|
+
|
|
735
750
|
/**
|
|
736
|
-
*
|
|
737
|
-
* @param
|
|
738
|
-
* @param
|
|
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.
|
|
739
754
|
*/
|
|
740
|
-
export function
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
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}`);
|
|
828
|
+
}
|
|
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
|
|
749
838
|
});
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
setData(result);
|
|
842
|
+
} catch (err) {
|
|
843
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
844
|
+
} finally {
|
|
845
|
+
setLoading(false);
|
|
846
|
+
}
|
|
847
|
+
}, [url, mergedCacheOptions]); // This dependency is now stable
|
|
848
|
+
|
|
849
|
+
useEffect(() => {
|
|
850
|
+
fetchData();
|
|
851
|
+
}, [fetchData]); // This dependency is now stable
|
|
852
|
+
|
|
853
|
+
return { data, loading, error, refetch: fetchData };
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* A hook for tracking window focus state.
|
|
858
|
+
* @returns {boolean} True if the window is focused.
|
|
859
|
+
*/
|
|
860
|
+
export function useWindowFocus(): boolean {
|
|
861
|
+
const [isFocused, setIsFocused] = useState(true);
|
|
862
|
+
|
|
863
|
+
useEffect(() => {
|
|
864
|
+
const onFocus = () => setIsFocused(true);
|
|
865
|
+
const onBlur = () => setIsFocused(false);
|
|
763
866
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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;
|
|
767
893
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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]);
|
|
774
925
|
}
|