the-react 1.0.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 ADDED
@@ -0,0 +1,2 @@
1
+ # My-React
2
+ React realization
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "the-react",
3
+ "version": "1.0.0",
4
+ "description": "A React library for TheBugs Project",
5
+ "main": "dist/my-react.umd.js",
6
+ "module": "dist/my-react.esm.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "vite build",
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/ArtemS18/My-React.git"
15
+ },
16
+ "keywords": [
17
+ "typescript",
18
+ "react",
19
+ "library",
20
+ "js",
21
+ "ts"
22
+ ],
23
+ "author": "Artemii",
24
+ "license": "ISC",
25
+ "bugs": {
26
+ "url": "https://github.com/ArtemS18/My-React/issues"
27
+ },
28
+ "homepage": "https://github.com/ArtemS18/My-React#readme",
29
+ "devDependencies": {
30
+ "typescript": "~5.9.3",
31
+ "vite": "^7.3.1",
32
+ "vite-tsconfig-paths": "^6.1.1"
33
+ }
34
+ }
@@ -0,0 +1,102 @@
1
+ import { ComponentInstance, markDirty } from "../my-react";
2
+
3
+ /**
4
+ * Глобальный контекст активного компонента для хуков.
5
+ * Устанавливается во время рендера компонента функцией _setActiveInstance.
6
+ */
7
+ let activeInstance: ComponentInstance<any> | undefined = undefined;
8
+
9
+ /** Индекс текущего состояния useState в массиве состояний компонента */
10
+ let activeStateIndex: number = 0;
11
+
12
+ /**
13
+ * Устанавливает активный экземпляр компонента для хуков.
14
+ * Вызывается автоматически в начале и конце updateVTree().
15
+ * @param instance - Текущий компонент или undefined (сброс).
16
+ * @internal
17
+ */
18
+ export function _setActiveInstance(instance: ComponentInstance<any> | undefined) {
19
+ activeInstance = instance;
20
+ }
21
+
22
+ /**
23
+ * Устанавливает индекс следующего состояния useState.
24
+ * Инкрементируется для каждого вызова useState в компоненте.
25
+ * @param stateIndex - Индекс состояния.
26
+ * @internal
27
+ */
28
+ export function _setActiveStateIndex(stateIndex: number) {
29
+ activeStateIndex = stateIndex;
30
+ }
31
+
32
+ /**
33
+ * Хук useState - управляет локальным состоянием компонента.
34
+ * Состояние сохраняется в массиве states компонента по индексу вызова.
35
+ *
36
+ * @param initialState - Начальное значение состояния.
37
+ * @returns Массив [текущее_состояние, setter_функция].
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * const [count, setCount] = useState(0);
42
+ * setCount(count + 1); // Обновит компонент через markDirty
43
+ * ```
44
+ */
45
+ export function useState<T>(
46
+ initialState: T
47
+ ): [T, (newState: T) => void] {
48
+ if (activeInstance === undefined) {
49
+ throw new Error("useState must be called inside a component");
50
+ }
51
+
52
+ if (activeInstance.states.length <= activeStateIndex) {
53
+ activeInstance.states.push(initialState);
54
+ }
55
+
56
+ const curInstance = activeInstance;
57
+ const idx = activeStateIndex;
58
+ activeStateIndex++;
59
+
60
+ return [
61
+ activeInstance.states[idx] as T,
62
+ (newState: T) => {
63
+ curInstance.states[idx] = newState;
64
+ markDirty(curInstance);
65
+ }
66
+ ];
67
+ }
68
+
69
+ /**
70
+ * Хук useEffect - выполняет побочные эффекты после рендера.
71
+ * Поддерживает cleanup функции и зависимости для оптимизации.
72
+ *
73
+ * @param effect - Функция эффекта. Может вернуть cleanup функцию.
74
+ * @param deps - Массив зависимостей. Если undefined - выполняется каждый рендер.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * useEffect(() => {
79
+ * const timer = setInterval(() => console.log('tick'), 1000);
80
+ * return () => clearInterval(timer); // cleanup
81
+ * }, []);
82
+ * ```
83
+ */
84
+ export function useEffect(
85
+ effect: () => void | (() => void),
86
+ deps?: any[]
87
+ ) {
88
+ if (activeInstance === undefined) {
89
+ throw new Error('useEffect must be called inside a component');
90
+ }
91
+
92
+ const idx = activeInstance.effectIndex++;
93
+ let eff = activeInstance.effects[idx];
94
+
95
+ if (!eff) {
96
+ eff = { execute: effect, deps };
97
+ activeInstance.effects[idx] = eff;
98
+ } else {
99
+ eff.execute = effect;
100
+ eff.deps = deps;
101
+ }
102
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export {createApp} from "./my-react";
2
+ export {type JSX} from "./types/jsx";
3
+ export { useState, useEffect } from "./hooks";
4
+ export * from "./jsx-runtime";
5
+ export * from "./router-dom";
@@ -0,0 +1 @@
1
+ export { jsx as jsxDEV } from '../jsx-runtime'; // Или ваша реализация jsxDEV
@@ -0,0 +1,102 @@
1
+ import type {
2
+ ChildrenType,
3
+ NormalizedChildrenType,
4
+ ComponentPropsType,
5
+ JSX,
6
+ JSXElement,
7
+ JSXElementType
8
+ } from "../types/jsx"
9
+
10
+ /**
11
+ * Нормализует children в плоский массив JSX-элементов и строк.
12
+ * Преобразует undefined → [], одиночные элементы → [element], массивы → flat().
13
+ * @param children - Исходные children из JSX.
14
+ * @returns Нормализованный массив для reconciler'а.
15
+ */
16
+ const normalizedChildren = (children: ChildrenType): NormalizedChildrenType => {
17
+ if (children === undefined || children === null) {
18
+ return [];
19
+ }
20
+ if (!Array.isArray(children)) {
21
+ return [children];
22
+ }
23
+ return children.flat().filter(child =>
24
+ child !== null && child !== undefined
25
+ );
26
+ }
27
+
28
+ /**
29
+ * Основная функция создания JSX-элементов и компонентов (jsx).
30
+ * Поддерживает создание DOM-элементов (string tag) и компонентов (function).
31
+ * Автоматически нормализует children и преобразует атрибуты.
32
+ *
33
+ * @param type - Тип элемента: string (div, span) или функция-компонент.
34
+ * @param props - Пропсы элемента/компонента + опциональные children.
35
+ * @param key - Уникальный ключ для reconciler'а (опционально).
36
+ * @returns JSXElement (DOM) или JSXComponent (компонент).
37
+ *
38
+ * @example Создание DOM-элемента:
39
+ * ```ts
40
+ * jsx('div', { className: 'container', children: 'Hello' }, 'unique-key')
41
+ * // → { type: 'element', tagName: 'div', attributes: Map, children: ['Hello'] }
42
+ * ```
43
+ *
44
+ * @example Создание компонента:
45
+ * ```ts
46
+ * const MyButton = (props) => jsx('button', props);
47
+ * jsx(MyButton, { text: 'Click me' }, 'btn-1')
48
+ * // → { type: 'component', func: MyButton, props, key: 'btn-1' }
49
+ * ```
50
+ */
51
+ function jsx<PropsType extends ComponentPropsType>(
52
+ type: string | ((props: PropsType) => any),
53
+ props: PropsType & { children?: ChildrenType },
54
+ key: string | undefined
55
+ ): JSXElementType {
56
+ if (typeof type === 'string') {
57
+ // Создаем DOM-элемент (div, span, button)
58
+ const attributes = new Map<string, any>();
59
+ Object.entries(props).forEach(([k, v]) => {
60
+ if (k !== "children") {
61
+ if (k === "disabled" && !v) {
62
+ return
63
+ }
64
+ if (k === "className") {
65
+ attributes.set("class", v)
66
+ } else {
67
+ attributes.set(k, v)
68
+ }
69
+ }
70
+ })
71
+ return {
72
+ type: "element",
73
+ tagName: type,
74
+ attributes: attributes,
75
+ children: normalizedChildren(props.children)
76
+ } as JSXElement
77
+ } else {
78
+ if (key === undefined) {
79
+ key = props.key as string
80
+ }
81
+ if (!key || key === null) {
82
+ const propsForHash = { ...props };
83
+ delete propsForHash.children;
84
+ key = `${type.name}_${stableHash(propsForHash)}`;
85
+ }
86
+ return {
87
+ type: "component",
88
+ key: key,
89
+ func: type,
90
+ props,
91
+ children: normalizedChildren(props.children)
92
+ }
93
+ }
94
+ }
95
+ function stableHash(obj: any): string {
96
+ return btoa(
97
+ JSON.stringify(obj, Object.keys(obj).sort())
98
+ ).slice(0, 8);
99
+ }
100
+
101
+ export type { JSX };
102
+ export { jsx, jsx as jsxs };
@@ -0,0 +1,509 @@
1
+ import type {
2
+ ComponentPropsType,
3
+ JSXElement,
4
+ JSXComponent,
5
+ JSXElementType,
6
+ KeyType
7
+ } from '../types/jsx'
8
+ import type {
9
+ DOMElement,
10
+ DOMTextNode
11
+ } from '../types/dom'
12
+
13
+ import {
14
+ _setActiveInstance,
15
+ _setActiveStateIndex
16
+ } from '../hooks/index'
17
+
18
+ /**
19
+ * Обновляет атрибуты DOM-элемента согласно новой карте атрибутов JSX.
20
+ * Сравнивает старые и новые атрибуты, удаляет устаревшие, добавляет новые,
21
+ * обрабатывает события и специальные атрибуты (style, value).
22
+ * @param repr - DOM-представление элемента.
23
+ * @param newAttrs - Новые атрибуты из JSX-дерева.
24
+ */
25
+ const patchAttributes = (repr: DOMElement, newAttrs: Map<string, any>) => {
26
+ repr.attrs.forEach((_, k) => {
27
+ if (!newAttrs.has(k)) {
28
+ repr.attrs.delete(k);
29
+ repr.elem.removeAttribute(k);
30
+ }
31
+ })
32
+
33
+ repr.attrs.forEach((v, k) => {
34
+ if (newAttrs.get(k) !== v) {
35
+ repr.attrs.set(k, newAttrs.get(k))
36
+ repr.elem.setAttribute(k, newAttrs.get(k));
37
+ }
38
+ })
39
+ repr.eventListeners.forEach((l) => {
40
+ repr.elem.removeEventListener(l.type, l.callback)
41
+ })
42
+ repr.eventListeners = []
43
+
44
+ newAttrs.forEach((v, k) => {
45
+ if (k.startsWith("on") && k[2] == k[2].toUpperCase()) {
46
+ const typeEvent = k.slice("on".length).toLocaleLowerCase()
47
+ repr.elem.addEventListener(typeEvent, v as () => void);
48
+ repr.eventListeners.push({type: typeEvent, callback: v})
49
+ } else if (k === "value" && repr.elem instanceof HTMLInputElement) {
50
+ repr.elem.value = v;
51
+ repr.attrs.set(k, v)
52
+ } else if (k === "style" && typeof v === "object" && v !== null) {
53
+ const oldStyle = repr.attrs.get("style") || {};
54
+ for (const prop in oldStyle) {
55
+ if (!(prop in v)) {
56
+ (repr.elem as any).style[prop] = "";
57
+ }
58
+ }
59
+ Object.assign((repr.elem as any).style, v);
60
+ repr.attrs.set(k, v);
61
+ } else if(!repr.attrs.has(k)) {
62
+ // Обычные атрибуты
63
+ repr.attrs.set(k, v)
64
+ repr.elem.setAttribute(k, v)
65
+ }
66
+ })
67
+ }
68
+
69
+ /** Массив очередей "грязных" компонентов по уровням глубины */
70
+ const dirtyInstances: Set<ComponentInstance<any>>[] = [];
71
+ let isUpdateScheduled = false;
72
+
73
+ /**
74
+ * Глубокое сравнение двух значений с поддержкой функций и вложенных объектов.
75
+ * @param val1 - Первое значение для сравнения.
76
+ * @param val2 - Второе значение для сравнения.
77
+ * @returns true если значения равны.
78
+ */
79
+ function deepEqual(val1: any, val2: any): boolean {
80
+ if (val1 === val2) return true;
81
+
82
+ if (typeof val1 === 'function' && typeof val2 === 'function') {
83
+ return true
84
+ }
85
+
86
+ if (val1 == null || val2 == null || typeof val1 !== 'object' || typeof val2 !== 'object') {
87
+ return val1 === val2;
88
+ }
89
+
90
+ if (Array.isArray(val1) && Array.isArray(val2)) {
91
+ if (val1.length !== val2.length) return false;
92
+ for (let i = 0; i < val1.length; i++) {
93
+ if (!deepEqual(val1[i], val2[i])) return false;
94
+ }
95
+ return true;
96
+ }
97
+
98
+ const keys1 = Object.keys(val1);
99
+ const keys2 = Object.keys(val2);
100
+
101
+ if (keys1.length !== keys2.length) return false;
102
+
103
+ for (const key of keys1) {
104
+ if (!Object.prototype.hasOwnProperty.call(val2, key) || !deepEqual(val1[key], val2[key])) {
105
+ return false;
106
+ }
107
+ }
108
+ return true;
109
+ }
110
+
111
+ /**
112
+ * Помечает компонент как "грязный" для обновления и планирует raf-обновление.
113
+ * @param instance - Компонент для обновления.
114
+ */
115
+ export const markDirty = (instance: ComponentInstance<any>) => {
116
+ while (dirtyInstances.length <= instance.depth) {
117
+ dirtyInstances.push(new Set());
118
+ }
119
+ dirtyInstances[instance.depth].add(instance);
120
+ if (!isUpdateScheduled) {
121
+ window.requestAnimationFrame(() => {
122
+ schedUpdate();
123
+ })
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Планировщик обновлений - обрабатывает "грязные" компоненты по уровням глубины.
129
+ * Использует requestAnimationFrame для батчинга обновлений.
130
+ */
131
+ const schedUpdate = () => {
132
+ isUpdateScheduled = true;
133
+ for (let i = 0; i < dirtyInstances.length; i++) {
134
+ if (dirtyInstances[i].size === 0) {
135
+ continue
136
+ }
137
+ dirtyInstances[i].forEach((instance) => {
138
+ instance.update();
139
+ dirtyInstances[i].delete(instance);
140
+ });
141
+ window.requestAnimationFrame(() => {
142
+ schedUpdate();
143
+ })
144
+ return
145
+ }
146
+
147
+ isUpdateScheduled = false;
148
+ };
149
+
150
+ /**
151
+ * Структура эффекта useEffect с зависимостями и cleanup функцией.
152
+ */
153
+ type Effect = {
154
+ execute: () => void | (() => void);
155
+ deps?: any[];
156
+ prevDeps?: any[];
157
+ cleanup?: () => void;
158
+ };
159
+
160
+ /**
161
+ * Экземпляр компонента React, управляющий жизненным циклом и DOM-патчингом.
162
+ * Содержит VTree, состояние, эффекты, дочерние компоненты и DOM-представление.
163
+ */
164
+ export class ComponentInstance<PropsType extends ComponentPropsType> {
165
+ /** Функция рендера компонента */
166
+ func: (props: PropsType) => any;
167
+ /** Карта дочерних компонентов по ключам */
168
+ instanceMap: Map<KeyType, ComponentInstance<any>>;
169
+ /** DOM-представление корневого элемента */
170
+ domElement: DOMElement | undefined;
171
+ /** Виртуальное дерево JSX */
172
+ vTree: JSXElement | undefined;
173
+ /** Пропсы компонента */
174
+ props: PropsType;
175
+ /** Массив состояний useState */
176
+ states: any[] = [];
177
+ /** Уровень глубины в дереве компонентов */
178
+ depth: number;
179
+ /** Родительский компонент */
180
+ parent: ComponentInstance<any> | undefined;
181
+ /** Массив эффектов useEffect */
182
+ effects: Effect[] = [];
183
+ /** Индекс текущего эффекта */
184
+
185
+ effectIndex: number = 0;
186
+
187
+ /**
188
+ * Создает новый экземпляр компонента.
189
+ * @param func - Функция рендера компонента.
190
+ * @param props - Пропсы компонента.
191
+ * @param parent - Родительский компонент.
192
+ */
193
+ constructor(
194
+ func: (props: PropsType) => any,
195
+ props: PropsType,
196
+ parent: ComponentInstance<any> | undefined
197
+ ) {
198
+ this.func = func;
199
+ this.props = props;
200
+ this.instanceMap = new Map();
201
+ this.parent = parent;
202
+ this.depth = (parent?.depth ?? 1) + 1;
203
+
204
+ this.update();
205
+ }
206
+
207
+ /** Полный цикл обновления компонента */
208
+ update() {
209
+ this.updateVTree();
210
+ if (this.vTree !== null){
211
+ this.patchInstances();
212
+ this.patchDOMNodes();
213
+ this.flushEffects();
214
+ }else{
215
+ this.destroy()
216
+ }
217
+
218
+ }
219
+
220
+ /** Пересоздает виртуальное дерево JSX */
221
+ updateVTree() {
222
+ _setActiveInstance(this)
223
+ _setActiveStateIndex(0)
224
+ this.effectIndex = 0;
225
+ this.vTree = this.func(this.props)
226
+ _setActiveInstance(undefined)
227
+ }
228
+
229
+ /**
230
+ * Извлекает виртуальные компоненты из JSX-дерева в карту.
231
+ * @param branch - Текущая ветка JSX-дерева.
232
+ * @param mapToAdd - Карта для добавления компонентов.
233
+ */
234
+ extractVirtualComponents(
235
+ branch: JSXElement,
236
+ mapToAdd: Map<KeyType, JSXComponent<any>>
237
+ ) {
238
+ branch.children.forEach((ch) => {
239
+ if (typeof ch === "string") {
240
+ return;
241
+ }
242
+ if (ch === undefined || ch === null) {
243
+ return;
244
+ }
245
+ if (ch.type == "element") {
246
+ this.extractVirtualComponents(ch, mapToAdd)
247
+ } else {
248
+ if (ch.key !== undefined) {
249
+ mapToAdd.set(ch.key, ch);
250
+ }
251
+ }
252
+ })
253
+ }
254
+
255
+ /** Синхронизирует дочерние компоненты с новым VTree */
256
+ patchInstances() {
257
+ if (this.vTree === undefined) {
258
+ throw new Error("vTree is undefined")
259
+ }
260
+ const newInstanceMap = new Map<KeyType, JSXComponent<any>>();
261
+ this.extractVirtualComponents(this.vTree, newInstanceMap);
262
+
263
+ this.instanceMap.forEach((v, k) => {
264
+ if (!newInstanceMap.has(k)) {
265
+ v.destroy();
266
+ this.instanceMap.delete(k);
267
+ }
268
+ });
269
+
270
+ this.instanceMap.forEach((v, k) => {
271
+ const newProps = (newInstanceMap.get(k) as JSXComponent<any>).props
272
+ if (!deepEqual(v.props, newProps)) {
273
+ v.props = newProps
274
+ v.update();
275
+ }
276
+ });
277
+
278
+ newInstanceMap.forEach((v, k) => {
279
+ if (!this.instanceMap.has(k)) {
280
+ this.instanceMap.set(k, new ComponentInstance<any>(v.func, v.props, this));
281
+ }
282
+ });
283
+ }
284
+
285
+ /** Выполняет эффекты useEffect с проверкой зависимостей */
286
+ flushEffects() {
287
+ for (const eff of this.effects) {
288
+ if (!eff) continue;
289
+
290
+ const depsChanged =
291
+ !eff.prevDeps ||
292
+ eff.deps?.length !== eff.prevDeps.length ||
293
+ eff.deps?.some((dep, i) => !deepEqual(dep, eff.prevDeps?.[i]));
294
+
295
+ if (depsChanged) {
296
+ if (eff.cleanup) {
297
+ eff.cleanup();
298
+ }
299
+ const cleanup = eff.execute();
300
+ if (typeof cleanup === 'function') {
301
+ eff.cleanup = cleanup;
302
+ } else {
303
+ eff.cleanup = undefined;
304
+ }
305
+ eff.prevDeps = eff.deps ? [...eff.deps] : undefined;
306
+ }
307
+ }
308
+ }
309
+
310
+ /** Патчит DOM согласно новому VTree */
311
+ patchDOMNodes() {
312
+ if (this.vTree === undefined) {
313
+ throw new Error()
314
+ }
315
+ const parentElem = this.domElement?.elem.parentElement;
316
+ if (this.domElement?.elem.tagName.toLowerCase() !== this.vTree.tagName) {
317
+ const prevChild = this.domElement?.elem
318
+ this.domElement = {
319
+ type: "element",
320
+ elem: document.createElement(this.vTree?.tagName),
321
+ attrs: new Map<string, any>(),
322
+ children: [],
323
+ eventListeners: [],
324
+ };
325
+
326
+ if (parentElem) {
327
+ parentElem.replaceChild(this.domElement.elem, prevChild as Node);
328
+ }
329
+ }
330
+ patchAttributes(this.domElement, this.vTree.attributes)
331
+ this.patchDOMNodesImpl(this.vTree.children, this.domElement.children, this.domElement.elem)
332
+ }
333
+
334
+ /**
335
+ * Рекурсивно патчит DOM-дерево согласно JSX-ветке.
336
+ * @param branch - Ветка JSX-дерева.
337
+ * @param domRepr - DOM-представление.
338
+ * @param parentElement - Родительский DOM-элемент.
339
+ */
340
+ patchDOMNodesImpl(
341
+ branch: (JSXElementType | string)[],
342
+ domRepr: (DOMElement | DOMTextNode)[],
343
+ parentElement: Element
344
+ ) {
345
+ let branchIndex = 0;
346
+ let domReprIndex = 0;
347
+
348
+ while(true) {
349
+ if (branch.length <= branchIndex) {
350
+ break;
351
+ }
352
+
353
+ const vNode = branch[branchIndex];
354
+ if (vNode === null || vNode === undefined) {
355
+ branchIndex++;
356
+ continue;
357
+ }
358
+ if (typeof vNode !== "string" && vNode?.type === undefined) {
359
+ branchIndex++;
360
+ continue
361
+ }
362
+
363
+ if (typeof vNode !== "string" && vNode.type === "component") {
364
+ const compInstance = this.instanceMap.get(vNode.key) as ComponentInstance<any>;
365
+ const compDom = compInstance.domElement;
366
+ console.log("compInstance", compInstance)
367
+ if (!compDom) {
368
+ branchIndex++;
369
+ continue
370
+ }
371
+
372
+ if (domReprIndex >= domRepr.length) {
373
+ domRepr.push(compDom);
374
+ } else if (domRepr[domReprIndex] !== compDom) {
375
+ domRepr.splice(domReprIndex, 0, compDom);
376
+ }
377
+
378
+ const currentNode = parentElement.childNodes[domReprIndex];
379
+ if (compDom.elem !== currentNode) {
380
+ if (currentNode) {
381
+ parentElement.insertBefore(compDom.elem, currentNode);
382
+ } else {
383
+ parentElement.appendChild(compDom.elem);
384
+ }
385
+ }
386
+
387
+ branchIndex++;
388
+ domReprIndex++;
389
+ continue;
390
+ }
391
+ if (domReprIndex >= domRepr.length) {
392
+ if (typeof vNode !== "string") {
393
+ domRepr.push({
394
+ type: "element",
395
+ attrs: new Map(),
396
+ elem: document.createElement(vNode.tagName),
397
+ children: [],
398
+ eventListeners: [],
399
+ })
400
+ } else {
401
+ domRepr.push({
402
+ type: "textNode",
403
+ text: vNode,
404
+ node: document.createTextNode(vNode),
405
+ });
406
+ }
407
+ }
408
+
409
+ const domNode = domRepr[domReprIndex];
410
+
411
+ if (typeof vNode === "string" && domNode.type === "element") {
412
+ domNode.elem.parentElement?.removeChild(domNode.elem);
413
+ domRepr.splice(domReprIndex, 1);
414
+ continue;
415
+ }
416
+
417
+ if (typeof vNode !== "string" && domNode.type === "textNode") {
418
+ domNode.node.parentElement?.removeChild(domNode.node);
419
+ domRepr.splice(domReprIndex, 1)
420
+ continue;
421
+ }
422
+
423
+ if (typeof vNode === "string" && domNode.type === "textNode") {
424
+ domNode.node.textContent = vNode;
425
+ const refNode = parentElement.childNodes[domReprIndex];
426
+ if (refNode) {
427
+ parentElement.insertBefore(domNode.node, refNode);
428
+ } else {
429
+ parentElement.appendChild(domNode.node);
430
+ }
431
+ branchIndex++;
432
+ domReprIndex++;
433
+ continue;
434
+ }
435
+
436
+ if (typeof vNode !== "string" && domNode.type !== "textNode") {
437
+ let elemRepr = domNode
438
+ if (domNode.elem.tagName.toLowerCase() !== vNode.tagName) {
439
+ const newElemRepr: DOMElement = {
440
+ type: "element",
441
+ attrs: new Map(),
442
+ elem: document.createElement(vNode.tagName),
443
+ children: [],
444
+ eventListeners: []
445
+ }
446
+ if (domNode.elem.parentElement === parentElement) {
447
+ parentElement.insertBefore(newElemRepr.elem, domNode.elem);
448
+ } else {
449
+ parentElement.appendChild(newElemRepr.elem);
450
+ }
451
+ domRepr.splice(domReprIndex, 0, newElemRepr);
452
+ elemRepr = newElemRepr
453
+ }
454
+ patchAttributes(elemRepr, vNode.attributes)
455
+ const currentNode = parentElement.childNodes[domReprIndex];
456
+ if (elemRepr.elem !== currentNode) {
457
+ if (currentNode) {
458
+ parentElement.insertBefore(elemRepr.elem, currentNode);
459
+ } else {
460
+ parentElement.appendChild(elemRepr.elem);
461
+ }
462
+ }
463
+
464
+ this.patchDOMNodesImpl(vNode.children, elemRepr.children, elemRepr.elem)
465
+ branchIndex++;
466
+ domReprIndex++;
467
+ }
468
+ }
469
+
470
+ while(domRepr.length > domReprIndex) {
471
+ const r = domRepr[domReprIndex]
472
+ if (r.type === "element") {
473
+ r.elem.parentElement?.removeChild(r.elem);
474
+ } else {
475
+ r.node.parentElement?.removeChild(r.node);
476
+ }
477
+ domRepr.splice(domReprIndex, 1);
478
+ }
479
+ }
480
+
481
+ /** Разрушает компонент и все дочерние */
482
+ destroy() {
483
+ for (const eff of this.effects) {
484
+ if (eff?.cleanup) {
485
+ eff.cleanup();
486
+ }
487
+ }
488
+ this.instanceMap.forEach((v) => {
489
+ v.destroy();
490
+ });
491
+ this.instanceMap.clear()
492
+ if (this.domElement?.elem.parentElement !== null) {
493
+ this.domElement?.elem.parentElement.removeChild(this.domElement.elem);
494
+ }
495
+ this.domElement = undefined;
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Создает React-приложение и монтирует его в DOM-элемент.
501
+ * @param elem - Контейнер DOM-элемент.
502
+ * @param fn - Функция рендера корневого компонента.
503
+ */
504
+ const createApp = (elem: Element, fn: () => JSXElementType) => {
505
+ const inst = new ComponentInstance<any>(fn, {}, undefined);
506
+ elem.appendChild(inst.domElement?.elem as Node);
507
+ }
508
+
509
+ export { createApp };
@@ -0,0 +1,79 @@
1
+ import type { JSXElementType } from "../types/jsx"
2
+
3
+ interface IRouterProps{
4
+ path: string
5
+ children: JSXElementType
6
+ currentPath: string
7
+ }
8
+
9
+ export function matchPath(pattern: string, path: string) {
10
+ const names: string[] = []
11
+ let regexStr = ''
12
+
13
+ for (let i = 0; i < pattern.length;) {
14
+ const ch = pattern[i]
15
+ if (ch === '{') {
16
+ const close = pattern.indexOf('}', i + 1)
17
+ if (close === -1) {
18
+ regexStr += '\\{'
19
+ i++
20
+ continue
21
+ }
22
+ const name = pattern.substring(i + 1, close)
23
+ names.push(name)
24
+ regexStr += '([^/]+)'
25
+ i = close + 1
26
+ } else {
27
+ if ('^$\\.*+?()[]|/'.includes(ch)) {
28
+ regexStr += '\\' + ch
29
+ } else {
30
+ regexStr += ch
31
+ }
32
+ i++
33
+ }
34
+ }
35
+
36
+ const regex = new RegExp('^' + regexStr + '$')
37
+ const m = regex.exec(path)
38
+ if (!m) return { matches: false, params: {} as Record<string, string> }
39
+
40
+ const params: Record<string, string> = {}
41
+ for (let i = 0; i < names.length; i++) {
42
+ params[names[i]] = decodeURIComponent(m[i + 1])
43
+ }
44
+ return { matches: true, params }
45
+ }
46
+
47
+ export function Router({path, children, currentPath}: IRouterProps){
48
+ const { matches, params } = matchPath(path, currentPath)
49
+ console.log(`Router ${path}:`, { currentPath, matches, params, children })
50
+
51
+ if (!matches && path != "*") return null
52
+
53
+ if (children.type === 'component') {
54
+ children.props = { ...children.props, ...params }
55
+ console.log(`render children ${path}`)
56
+ return (<div>{children}</div>)
57
+ }
58
+
59
+ return (<div>{children}</div>)
60
+ }
61
+
62
+ interface ISwitchrProps{
63
+ children: any[]
64
+ currentPath: string
65
+ }
66
+
67
+ export function Switch({ currentPath, children }: ISwitchrProps) {
68
+ const childrenArray = Array.isArray(children) ? children : [children];
69
+
70
+ for (const child of childrenArray) {
71
+ if (child && child.type === 'component' && child.props.path) {
72
+ const { matches } = matchPath(child.props.path, currentPath);
73
+ if (matches || child.props.path == "*") {
74
+ return <div>{child}</div>;
75
+ }
76
+ }
77
+ }
78
+ return null;
79
+ }
@@ -0,0 +1,8 @@
1
+ export function useNavigate() {
2
+ const navigate = (path: string) => {
3
+ console.log(path)
4
+ window.history.pushState({}, '', path);
5
+ window.dispatchEvent(new PopStateEvent('popstate'));
6
+ };
7
+ return navigate;
8
+ }
@@ -0,0 +1,2 @@
1
+ export { Router, Switch } from "./Router";
2
+ export { useNavigate } from "./hooks";
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Представление DOM-элемента в виртуальном DOM.
3
+ * Содержит реальный DOM-узел, атрибуты, дочерние элементы и обработчики событий.
4
+ */
5
+ export interface DOMElement {
6
+ /** Тип узла */
7
+ type: "element";
8
+ /** Реальный DOM-элемент браузера */
9
+ elem: Element;
10
+ /** Карта атрибутов элемента (ключ-значение) */
11
+ attrs: Map<string, any>;
12
+ /** Массив дочерних DOM-узлов */
13
+ children: (DOMElement | DOMTextNode)[];
14
+ /** Массив обработчиков событий элемента */
15
+ eventListeners: { type: string; callback: () => void; }[];
16
+ }
17
+
18
+ /**
19
+ * Представление текстового DOM-узла в виртуальном DOM.
20
+ * Используется для рендера строкового контента между элементами.
21
+ */
22
+ export interface DOMTextNode {
23
+ /** Тип узла */
24
+ type: "textNode";
25
+ /** Текстовое содержимое */
26
+ text: string;
27
+ /** Реальный DOM текстовый узел */
28
+ node: Node;
29
+ }
@@ -0,0 +1,39 @@
1
+ type any1 = any;
2
+ export type KeyType = string;
3
+
4
+ export namespace JSX{
5
+ export interface IntrinsicAttributes extends any1{
6
+ key?: KeyType;
7
+ }
8
+ export type SVGAttributes = any;
9
+ export type HTMLAttributes = any;
10
+ export interface IntrinsicElements extends any1{}
11
+ }
12
+ export interface ComponentPropsType {
13
+ key: KeyType;
14
+ onClick: ()=>any | undefined;
15
+ }
16
+
17
+ export interface JSXElement {
18
+ type: "element"
19
+ tagName: string;
20
+ attributes: Map<string, any>;
21
+ children: NormalizedChildrenType;
22
+ }
23
+ export interface JSXComponent<PropsType extends ComponentPropsType>{
24
+ type: "component";
25
+ func: (props: any) => JSXElementType;
26
+ key: KeyType;
27
+ props: PropsType;
28
+ children: NormalizedChildrenType;
29
+ }
30
+
31
+ export type JSXElementType = JSXElement | JSXComponent<any> ;
32
+
33
+ export type ChildrenType =
34
+ | JSXElementType
35
+ | string
36
+ | (JSXElementType | string)[]
37
+ | undefined;
38
+
39
+ export type NormalizedChildrenType = (JSXElementType | string)[];
package/tsconfig.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "module": "ESNext",
7
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true,
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "verbatimModuleSyntax": true,
13
+ "moduleDetection": "force",
14
+ "paths":{
15
+ "@my-react/*": ["./src/*"]
16
+ },
17
+ "jsx": "react-jsx",
18
+ "jsxImportSource": "@my-react",
19
+ "noEmit": true,
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "erasableSyntaxOnly": true,
24
+ "noFallthroughCasesInSwitch": true,
25
+ "noUncheckedSideEffectImports": true
26
+ },
27
+ "include": ["src"]
28
+ }