jsx-framework-test-pb 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/reactivity/effect.d.ts +2 -2
- package/dist/reactivity/effect.js +21 -2
- package/dist/reactivity/index.d.ts +1 -1
- package/dist/reactivity/state.d.ts +2 -0
- package/dist/reactivity/state.js +30 -2
- package/dist/reactivity/types.d.ts +2 -1
- package/dist/runtime/createElement.d.ts +1 -0
- package/dist/runtime/createElement.js +79 -8
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/reconciler.js +17 -2
- package/dist/runtime/render.js +7 -0
- package/package.json +1 -1
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { Effect, StateObject } from './types';
|
|
2
|
-
export declare function effect(fn: Effect):
|
|
1
|
+
import type { Effect, StateObject, Cleanup } from './types';
|
|
2
|
+
export declare function effect(fn: Effect): Cleanup;
|
|
3
3
|
export declare function computed<T>(fn: () => T): StateObject<T>;
|
|
@@ -1,11 +1,30 @@
|
|
|
1
|
-
import { setActiveEffect, state } from './state';
|
|
1
|
+
import { setActiveEffect, setActiveDeps, state } from './state';
|
|
2
2
|
export function effect(fn) {
|
|
3
|
+
let cleanup;
|
|
4
|
+
const deps = new Set();
|
|
3
5
|
const runner = () => {
|
|
6
|
+
// Clear old subscriptions before re-running
|
|
7
|
+
deps.forEach(sub => sub.delete(runner));
|
|
8
|
+
deps.clear();
|
|
9
|
+
// Run cleanup from previous execution
|
|
10
|
+
cleanup?.();
|
|
11
|
+
// Set context for dependency tracking
|
|
4
12
|
setActiveEffect(runner);
|
|
5
|
-
|
|
13
|
+
setActiveDeps(deps);
|
|
14
|
+
// Execute effect and capture any returned cleanup
|
|
15
|
+
const result = fn();
|
|
16
|
+
cleanup = typeof result === 'function' ? result : undefined;
|
|
17
|
+
// Clear context
|
|
6
18
|
setActiveEffect(null);
|
|
19
|
+
setActiveDeps(null);
|
|
7
20
|
};
|
|
8
21
|
runner();
|
|
22
|
+
// Return dispose function
|
|
23
|
+
return () => {
|
|
24
|
+
cleanup?.();
|
|
25
|
+
deps.forEach(sub => sub.delete(runner));
|
|
26
|
+
deps.clear();
|
|
27
|
+
};
|
|
9
28
|
}
|
|
10
29
|
export function computed(fn) {
|
|
11
30
|
const s = state(undefined);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Effect, StateObject } from './types';
|
|
2
2
|
export declare function setActiveEffect(effect: Effect | null): void;
|
|
3
3
|
export declare function getActiveEffect(): Effect | null;
|
|
4
|
+
export declare function setActiveDeps(deps: Set<Set<Effect>> | null): void;
|
|
5
|
+
export declare function getActiveDeps(): Set<Set<Effect>> | null;
|
|
4
6
|
export declare function state<T>(initial: T): StateObject<T>;
|
|
5
7
|
export declare function isState(value: unknown): value is StateObject<unknown>;
|
package/dist/reactivity/state.js
CHANGED
|
@@ -1,24 +1,52 @@
|
|
|
1
1
|
let activeEffect = null;
|
|
2
|
+
let activeDeps = null;
|
|
3
|
+
// Batching state
|
|
4
|
+
let pendingEffects = new Set();
|
|
5
|
+
let isFlushing = false;
|
|
2
6
|
export function setActiveEffect(effect) {
|
|
3
7
|
activeEffect = effect;
|
|
4
8
|
}
|
|
5
9
|
export function getActiveEffect() {
|
|
6
10
|
return activeEffect;
|
|
7
11
|
}
|
|
12
|
+
export function setActiveDeps(deps) {
|
|
13
|
+
activeDeps = deps;
|
|
14
|
+
}
|
|
15
|
+
export function getActiveDeps() {
|
|
16
|
+
return activeDeps;
|
|
17
|
+
}
|
|
18
|
+
function queueEffect(effect) {
|
|
19
|
+
pendingEffects.add(effect);
|
|
20
|
+
if (!isFlushing) {
|
|
21
|
+
isFlushing = true;
|
|
22
|
+
queueMicrotask(flushEffects);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function flushEffects() {
|
|
26
|
+
const effects = pendingEffects;
|
|
27
|
+
pendingEffects = new Set();
|
|
28
|
+
isFlushing = false;
|
|
29
|
+
effects.forEach(fn => fn());
|
|
30
|
+
}
|
|
8
31
|
export function state(initial) {
|
|
9
32
|
let value = initial;
|
|
10
33
|
const subscribers = new Set();
|
|
11
34
|
return {
|
|
12
35
|
get val() {
|
|
13
|
-
if (activeEffect)
|
|
36
|
+
if (activeEffect) {
|
|
14
37
|
subscribers.add(activeEffect);
|
|
38
|
+
// Track this subscriber set in the effect's dependencies
|
|
39
|
+
if (activeDeps) {
|
|
40
|
+
activeDeps.add(subscribers);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
15
43
|
return value;
|
|
16
44
|
},
|
|
17
45
|
set val(next) {
|
|
18
46
|
if (Object.is(value, next))
|
|
19
47
|
return;
|
|
20
48
|
value = next;
|
|
21
|
-
subscribers.forEach(fn => fn
|
|
49
|
+
subscribers.forEach(fn => queueEffect(fn));
|
|
22
50
|
}
|
|
23
51
|
};
|
|
24
52
|
}
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { isElement, isValidChild, FRAGMENT_TYPE } from '../utils';
|
|
2
2
|
import { effect } from '../reactivity';
|
|
3
|
+
// Track cleanups per node for proper disposal
|
|
4
|
+
const nodeCleanups = new WeakMap();
|
|
5
|
+
function trackCleanup(node, cleanup) {
|
|
6
|
+
let cleanups = nodeCleanups.get(node);
|
|
7
|
+
if (!cleanups) {
|
|
8
|
+
cleanups = new Set();
|
|
9
|
+
nodeCleanups.set(node, cleanups);
|
|
10
|
+
}
|
|
11
|
+
cleanups.add(cleanup);
|
|
12
|
+
}
|
|
13
|
+
export function cleanupNode(node) {
|
|
14
|
+
const cleanups = nodeCleanups.get(node);
|
|
15
|
+
if (cleanups) {
|
|
16
|
+
cleanups.forEach(fn => fn());
|
|
17
|
+
cleanups.clear();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
3
20
|
export function createElement(element) {
|
|
4
21
|
if (!isValidChild(element)) {
|
|
5
22
|
return null;
|
|
6
23
|
}
|
|
7
24
|
if (typeof element === 'function') {
|
|
8
|
-
|
|
9
|
-
effect(() => {
|
|
10
|
-
textNode.textContent = String(element());
|
|
11
|
-
});
|
|
12
|
-
return textNode;
|
|
25
|
+
return createReactiveChild(element);
|
|
13
26
|
}
|
|
14
27
|
if (typeof element === 'string' || typeof element === 'number') {
|
|
15
28
|
return document.createTextNode(String(element));
|
|
@@ -33,6 +46,57 @@ export function createElement(element) {
|
|
|
33
46
|
}
|
|
34
47
|
return null;
|
|
35
48
|
}
|
|
49
|
+
function createReactiveChild(fn) {
|
|
50
|
+
// Use a comment as a stable anchor point
|
|
51
|
+
const anchor = document.createComment('');
|
|
52
|
+
let currentNode = null;
|
|
53
|
+
let isFirstRun = true;
|
|
54
|
+
const cleanup = effect(() => {
|
|
55
|
+
const value = fn();
|
|
56
|
+
// Clean up previous node (skip on first run)
|
|
57
|
+
if (!isFirstRun && currentNode) {
|
|
58
|
+
cleanupTree(currentNode);
|
|
59
|
+
currentNode.parentNode?.removeChild(currentNode);
|
|
60
|
+
currentNode = null;
|
|
61
|
+
}
|
|
62
|
+
// Handle different return types
|
|
63
|
+
if (value == null || value === false) {
|
|
64
|
+
currentNode = null;
|
|
65
|
+
}
|
|
66
|
+
else if (typeof value === 'string' || typeof value === 'number') {
|
|
67
|
+
currentNode = document.createTextNode(String(value));
|
|
68
|
+
}
|
|
69
|
+
else if (isElement(value) || Array.isArray(value)) {
|
|
70
|
+
currentNode = createElement(value);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
currentNode = document.createTextNode(String(value));
|
|
74
|
+
}
|
|
75
|
+
// Insert new node after anchor (skip on first run - handled by fragment)
|
|
76
|
+
if (!isFirstRun && currentNode && anchor.parentNode) {
|
|
77
|
+
anchor.parentNode.insertBefore(currentNode, anchor.nextSibling);
|
|
78
|
+
}
|
|
79
|
+
isFirstRun = false;
|
|
80
|
+
});
|
|
81
|
+
trackCleanup(anchor, () => {
|
|
82
|
+
cleanup();
|
|
83
|
+
if (currentNode) {
|
|
84
|
+
cleanupTree(currentNode);
|
|
85
|
+
currentNode.parentNode?.removeChild(currentNode);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
// Return fragment with anchor and initial node
|
|
89
|
+
const fragment = document.createDocumentFragment();
|
|
90
|
+
fragment.appendChild(anchor);
|
|
91
|
+
if (currentNode) {
|
|
92
|
+
fragment.appendChild(currentNode);
|
|
93
|
+
}
|
|
94
|
+
return fragment;
|
|
95
|
+
}
|
|
96
|
+
function cleanupTree(node) {
|
|
97
|
+
cleanupNode(node);
|
|
98
|
+
node.childNodes.forEach(child => cleanupTree(child));
|
|
99
|
+
}
|
|
36
100
|
function createFragment(children) {
|
|
37
101
|
const fragment = document.createDocumentFragment();
|
|
38
102
|
const childArray = Array.isArray(children) ? children : [children];
|
|
@@ -70,9 +134,10 @@ function createHTMLElement(type, props) {
|
|
|
70
134
|
}
|
|
71
135
|
function handleReactiveProp(element, attrName, value) {
|
|
72
136
|
if (typeof value === 'function' && !attrName.startsWith('on')) {
|
|
73
|
-
effect(() => {
|
|
137
|
+
const cleanup = effect(() => {
|
|
74
138
|
element.setAttribute(attrName, String(value()));
|
|
75
139
|
});
|
|
140
|
+
trackCleanup(element, cleanup);
|
|
76
141
|
}
|
|
77
142
|
else {
|
|
78
143
|
element.setAttribute(attrName, String(value));
|
|
@@ -89,15 +154,21 @@ function appendChildren(parent, children) {
|
|
|
89
154
|
}
|
|
90
155
|
function attachEvent(element, key, handler) {
|
|
91
156
|
const eventName = key.substring(2).toLowerCase();
|
|
92
|
-
|
|
157
|
+
const listener = handler;
|
|
158
|
+
element.addEventListener(eventName, listener);
|
|
159
|
+
// Track for cleanup on unmount
|
|
160
|
+
trackCleanup(element, () => {
|
|
161
|
+
element.removeEventListener(eventName, listener);
|
|
162
|
+
});
|
|
93
163
|
}
|
|
94
164
|
function applyStyles(element, styles) {
|
|
95
165
|
Object.keys(styles).forEach(key => {
|
|
96
166
|
const value = styles[key];
|
|
97
167
|
if (typeof value === 'function') {
|
|
98
|
-
effect(() => {
|
|
168
|
+
const cleanup = effect(() => {
|
|
99
169
|
element.style[key] = value();
|
|
100
170
|
});
|
|
171
|
+
trackCleanup(element, cleanup);
|
|
101
172
|
}
|
|
102
173
|
else {
|
|
103
174
|
element.style[key] = value;
|
package/dist/runtime/index.d.ts
CHANGED
package/dist/runtime/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createElement } from "./createElement";
|
|
1
|
+
import { createElement, cleanupNode } from "./createElement";
|
|
2
2
|
import { isElement } from "../utils";
|
|
3
3
|
import { effect } from "../reactivity";
|
|
4
4
|
function canReuse(oldElement, newElement) {
|
|
@@ -40,6 +40,10 @@ function setAttr(el, name, value) {
|
|
|
40
40
|
function getInsertionPoint(parent, targetIndex) {
|
|
41
41
|
return parent.childNodes.item(targetIndex) ?? null;
|
|
42
42
|
}
|
|
43
|
+
function cleanupTree(node) {
|
|
44
|
+
cleanupNode(node);
|
|
45
|
+
node.childNodes.forEach(child => cleanupTree(child));
|
|
46
|
+
}
|
|
43
47
|
export function reconcile(parent, newElements, oldFibers = []) {
|
|
44
48
|
const newFibers = [];
|
|
45
49
|
const newElementsArray = Array.isArray(newElements) ? newElements : [newElements];
|
|
@@ -66,6 +70,8 @@ export function reconcile(parent, newElements, oldFibers = []) {
|
|
|
66
70
|
oldFibersByIndex.delete(index);
|
|
67
71
|
}
|
|
68
72
|
let fiber;
|
|
73
|
+
let isNewFiber = false;
|
|
74
|
+
let needsMove = false;
|
|
69
75
|
if (oldFiber && canReuse(oldFiber.element, element)) {
|
|
70
76
|
fiber = { element, dom: oldFiber.dom, key, index };
|
|
71
77
|
if (fiber.dom)
|
|
@@ -73,19 +79,26 @@ export function reconcile(parent, newElements, oldFibers = []) {
|
|
|
73
79
|
if (oldFiber.dom && isElement(element)) {
|
|
74
80
|
updateElement(oldFiber.dom, element);
|
|
75
81
|
}
|
|
82
|
+
// Only need to move if position changed
|
|
83
|
+
needsMove = oldFiber.index !== index;
|
|
76
84
|
}
|
|
77
85
|
else {
|
|
78
86
|
const dom = createElement(element);
|
|
79
87
|
fiber = { element, dom, key, index };
|
|
88
|
+
isNewFiber = true;
|
|
89
|
+
// Clean up old fiber if it existed but couldn't be reused
|
|
80
90
|
if (oldFiber?.dom?.parentNode === parent) {
|
|
91
|
+
cleanupTree(oldFiber.dom);
|
|
81
92
|
parent.removeChild(oldFiber.dom);
|
|
82
93
|
}
|
|
94
|
+
// Insert new element
|
|
83
95
|
if (dom) {
|
|
84
96
|
const before = getInsertionPoint(parent, index);
|
|
85
97
|
parent.insertBefore(dom, before);
|
|
86
98
|
}
|
|
87
99
|
}
|
|
88
|
-
|
|
100
|
+
// Only reposition reused fibers that actually need to move
|
|
101
|
+
if (!isNewFiber && needsMove && fiber.dom) {
|
|
89
102
|
const before = getInsertionPoint(parent, index);
|
|
90
103
|
if (fiber.dom !== before) {
|
|
91
104
|
parent.insertBefore(fiber.dom, before);
|
|
@@ -93,8 +106,10 @@ export function reconcile(parent, newElements, oldFibers = []) {
|
|
|
93
106
|
}
|
|
94
107
|
newFibers.push(fiber);
|
|
95
108
|
});
|
|
109
|
+
// Clean up remaining old fibers
|
|
96
110
|
[...oldFibersByKey.values(), ...oldFibersByIndex.values()].forEach((fiber) => {
|
|
97
111
|
if (fiber.dom?.parentNode === parent && !reusedOldDoms.has(fiber.dom)) {
|
|
112
|
+
cleanupTree(fiber.dom);
|
|
98
113
|
parent.removeChild(fiber.dom);
|
|
99
114
|
}
|
|
100
115
|
});
|
package/dist/runtime/render.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { reconcile } from './reconciler';
|
|
2
2
|
import { isElement } from '../utils';
|
|
3
|
+
import { cleanupNode } from './createElement';
|
|
3
4
|
const instances = new WeakMap();
|
|
4
5
|
export function render(element, container) {
|
|
5
6
|
if (!container)
|
|
@@ -16,9 +17,15 @@ export function render(element, container) {
|
|
|
16
17
|
const newFibers = reconcile(container, children, oldFibers);
|
|
17
18
|
instances.set(container, newFibers);
|
|
18
19
|
}
|
|
20
|
+
function cleanupTree(node) {
|
|
21
|
+
cleanupNode(node);
|
|
22
|
+
node.childNodes.forEach(child => cleanupTree(child));
|
|
23
|
+
}
|
|
19
24
|
export function unmount(container) {
|
|
20
25
|
if (!container)
|
|
21
26
|
return;
|
|
27
|
+
// Clean up all nodes recursively before removing
|
|
28
|
+
cleanupTree(container);
|
|
22
29
|
container.replaceChildren();
|
|
23
30
|
instances.delete(container);
|
|
24
31
|
}
|