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.
@@ -1,3 +1,3 @@
1
- import type { Effect, StateObject } from './types';
2
- export declare function effect(fn: Effect): void;
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
- fn();
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,3 +1,3 @@
1
- export type { Effect, StateObject } from './types';
1
+ export type { Cleanup, Effect, StateObject } from './types';
2
2
  export { state, isState } from './state';
3
3
  export { effect, computed } from './effect';
@@ -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>;
@@ -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,4 +1,5 @@
1
- export type Effect = () => void;
1
+ export type Cleanup = () => void;
2
+ export type Effect = () => void | Cleanup;
2
3
  export interface StateObject<T> {
3
4
  val: T;
4
5
  }
@@ -1 +1,2 @@
1
+ export declare function cleanupNode(node: Node): void;
1
2
  export declare function createElement(element: any): Node | null;
@@ -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
- const textNode = document.createTextNode('');
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
- element.addEventListener(eventName, handler);
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;
@@ -1,4 +1,4 @@
1
1
  export type { FiberNode } from './types';
2
2
  export { reconcile } from './reconciler';
3
3
  export { render, unmount } from './render';
4
- export { createElement } from './createElement';
4
+ export { createElement, cleanupNode } from './createElement';
@@ -1,3 +1,3 @@
1
1
  export { reconcile } from './reconciler';
2
2
  export { render, unmount } from './render';
3
- export { createElement } from './createElement';
3
+ export { createElement, cleanupNode } from './createElement';
@@ -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
- if (fiber.dom) {
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
  });
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsx-framework-test-pb",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",