@zhinnx/core 2.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/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from './src/reactive.js';
2
+ export * from './src/vdom.js';
3
+ export * from './src/diff.js';
4
+ export * from './src/Component.js';
5
+ export * from './src/Lazy.js';
6
+ export * from './src/Router.js';
7
+ export * from './src/Store.js';
8
+ export * from './src/API.js';
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@zhinnx/core",
3
+ "version": "2.0.0",
4
+ "description": "Core library for zhinnx framework",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "exports": {
8
+ ".": "./index.js"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "keywords": ["zhinnx", "framework", "vdom", "reactive"],
14
+ "license": "MIT"
15
+ }
package/src/API.js ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * zhinnx Core - API
3
+ * A unified wrapper for making API requests.
4
+ */
5
+
6
+ export class API {
7
+ constructor(baseURL = '') {
8
+ this.baseURL = baseURL;
9
+ }
10
+
11
+ async request(endpoint, method = 'GET', data = null) {
12
+ const config = {
13
+ method,
14
+ headers: {
15
+ 'Content-Type': 'application/json',
16
+ },
17
+ };
18
+
19
+ if (data) {
20
+ config.body = JSON.stringify(data);
21
+ }
22
+
23
+ try {
24
+ const response = await fetch(`${this.baseURL}${endpoint}`, config);
25
+
26
+ if (!response.ok) {
27
+ throw new Error(`API Error: ${response.status} ${response.statusText}`);
28
+ }
29
+
30
+ return await response.json();
31
+ } catch (error) {
32
+ console.error('API Request Failed:', error);
33
+ throw error;
34
+ }
35
+ }
36
+
37
+ get(endpoint) {
38
+ return this.request(endpoint, 'GET');
39
+ }
40
+
41
+ post(endpoint, data) {
42
+ return this.request(endpoint, 'POST', data);
43
+ }
44
+
45
+ put(endpoint, data) {
46
+ return this.request(endpoint, 'PUT', data);
47
+ }
48
+
49
+ delete(endpoint) {
50
+ return this.request(endpoint, 'DELETE');
51
+ }
52
+ }
53
+
54
+ // Export a default instance
55
+ export default new API();
@@ -0,0 +1,117 @@
1
+ /**
2
+ * zhinnx Core - Component
3
+ * Base class with Reactivity and VDOM integration.
4
+ */
5
+
6
+ import { reactive, effect } from './reactive.js';
7
+ import { html } from './vdom.js';
8
+ import { diffChildren, unmount, hydrate } from './diff.js';
9
+
10
+ export { html };
11
+
12
+ export class Component {
13
+ constructor(props = {}) {
14
+ this.props = props;
15
+ this.state = reactive({});
16
+ this.isMounted = false;
17
+
18
+ // Internal
19
+ this._container = null;
20
+ this._vnodes = []; // Supports Fragments (multiple roots)
21
+ this._updateEffect = null;
22
+ }
23
+
24
+ /**
25
+ * Override to return VNodes.
26
+ */
27
+ render() {
28
+ return html`<div></div>`;
29
+ }
30
+
31
+ /**
32
+ * Mounts the component to a DOM element.
33
+ */
34
+ mount(container) {
35
+ if (this.isMounted) return;
36
+ this._container = container;
37
+
38
+ // Check for Hydration Need (SSR Content Present)
39
+ // Only hydrate if we haven't mounted yet and container has children
40
+ const shouldHydrate = !this.isMounted && container.hasChildNodes();
41
+
42
+ // Reactive update loop
43
+ this._updateEffect = effect(() => {
44
+ // If it's the first run and shouldHydrate is true, run hydration
45
+ // Note: In effect, this runs immediately.
46
+ if (shouldHydrate && !this.isMounted) {
47
+ this.hydrate();
48
+ } else {
49
+ this.update();
50
+ }
51
+ });
52
+
53
+ this.isMounted = true;
54
+ this.onMount();
55
+ }
56
+
57
+ hydrate() {
58
+ if (!this._container) return;
59
+ const rendered = this.render();
60
+ const newVNodes = Array.isArray(rendered) ? rendered : [rendered];
61
+
62
+ // Hydrate logic in diff.js
63
+ hydrate(newVNodes, this._container);
64
+
65
+ this._vnodes = newVNodes;
66
+ this.afterRender();
67
+ }
68
+
69
+ /**
70
+ * Force update (automatically called by reactive state).
71
+ */
72
+ update() {
73
+ if (!this._container) return;
74
+
75
+ const rendered = this.render();
76
+ // Normalize to array to support Fragments (multiple root nodes)
77
+ const newVNodes = Array.isArray(rendered) ? rendered : [rendered];
78
+
79
+ // Use diffChildren to reconcile the container's content
80
+ diffChildren(this._vnodes, newVNodes, this._container);
81
+
82
+ this._vnodes = newVNodes;
83
+
84
+ // Lifecycle
85
+ this.afterRender();
86
+ }
87
+
88
+ unmount() {
89
+ if (!this.isMounted) return;
90
+
91
+ this._vnodes.forEach(vnode => unmount(vnode));
92
+ this._vnodes = [];
93
+ this.isMounted = false;
94
+ this.onUnmount();
95
+ }
96
+
97
+ /**
98
+ * Legacy State Support
99
+ */
100
+ setState(newState) {
101
+ Object.assign(this.state, newState);
102
+ }
103
+
104
+ /**
105
+ * Lifecycle Hooks
106
+ */
107
+ onMount() {}
108
+ onUnmount() {}
109
+ afterRender() {}
110
+
111
+ /**
112
+ * Helper
113
+ */
114
+ $(selector) {
115
+ return this._container ? this._container.querySelector(selector) : null;
116
+ }
117
+ }
package/src/Lazy.js ADDED
@@ -0,0 +1,69 @@
1
+
2
+ /**
3
+ * zhinnx Lazy Hydration Wrapper
4
+ */
5
+ import { Component, html } from './Component.js';
6
+
7
+ export class Lazy extends Component {
8
+ constructor(props) {
9
+ super(props);
10
+ this.state = {
11
+ isLoaded: false,
12
+ component: null
13
+ };
14
+ this.observer = null;
15
+ }
16
+
17
+ render() {
18
+ if (this.state.isLoaded && this.state.component) {
19
+ // Render the actual loaded component
20
+ // We need to instantiate it if it's a class
21
+ const Comp = this.state.component;
22
+ return new Comp(this.props).render();
23
+ }
24
+
25
+ // Placeholder
26
+ return html`<div class="zhin-lazy-placeholder" style="min-height: 100px">Loading...</div>`;
27
+ }
28
+
29
+ onMount() {
30
+ // Use IntersectionObserver to detect visibility
31
+ if ('IntersectionObserver' in window) {
32
+ this.observer = new IntersectionObserver((entries) => {
33
+ entries.forEach(entry => {
34
+ if (entry.isIntersecting) {
35
+ this.loadComponent();
36
+ this.observer.disconnect();
37
+ }
38
+ });
39
+ });
40
+
41
+ const el = this.$('.zhin-lazy-placeholder');
42
+ if (el) this.observer.observe(el);
43
+ else this.loadComponent(); // If no placeholder (maybe rendered already?), load immediately
44
+ } else {
45
+ // Fallback for no observer
46
+ this.loadComponent();
47
+ }
48
+ }
49
+
50
+ async loadComponent() {
51
+ if (this.state.isLoaded) return;
52
+
53
+ const loader = this.props.loader;
54
+ if (loader) {
55
+ try {
56
+ // Expect loader to be a function returning a Promise that resolves to a Module or Component
57
+ const module = await loader();
58
+ const Comp = module.default || module;
59
+ this.setState({ isLoaded: true, component: Comp });
60
+ } catch (e) {
61
+ console.error('Lazy Load Error:', e);
62
+ }
63
+ }
64
+ }
65
+
66
+ onUnmount() {
67
+ if (this.observer) this.observer.disconnect();
68
+ }
69
+ }
package/src/Router.js ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * zhinnx Core - Router
3
+ * specific Simple client-side routing.
4
+ */
5
+
6
+ export class Router {
7
+ /**
8
+ * @param {Object} routeMap - Map of routes from server { '/path': { regex, importPath, params } }
9
+ * @param {HTMLElement} rootElement - The DOM element to render pages into.
10
+ */
11
+ constructor(routeMap, rootElement) {
12
+ this.routeMap = routeMap || {};
13
+ this.root = rootElement;
14
+ this.hydrated = false;
15
+
16
+ // Use History API for standard routing
17
+ window.addEventListener('popstate', () => this.resolve());
18
+
19
+ // Initial load - if DOM already loaded (scripts at end of body), resolve immediately
20
+ if (document.readyState === 'loading') {
21
+ window.addEventListener('DOMContentLoaded', () => this.resolve());
22
+ } else {
23
+ this.resolve();
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Resolve the current route and render the component.
29
+ */
30
+ resolve() {
31
+ const path = window.location.pathname || '/';
32
+
33
+ // Find matching route using Regex
34
+ let matchedRoute = null;
35
+ let params = {};
36
+ let matchedKey = null;
37
+
38
+ // routeMap keys are simple paths, but values contain regex
39
+ for (const [key, route] of Object.entries(this.routeMap)) {
40
+ const re = new RegExp(route.regex);
41
+ const match = path.match(re);
42
+ if (match) {
43
+ matchedKey = key;
44
+ matchedRoute = route;
45
+ // Extract params
46
+ if (route.params) {
47
+ route.params.forEach((key, index) => {
48
+ params[key] = match[index + 1];
49
+ });
50
+ }
51
+ break;
52
+ }
53
+ }
54
+
55
+ if (matchedRoute) {
56
+ const handleComponent = (ComponentClass) => {
57
+ const shouldHydrate = this.root.hasChildNodes() && !this.hydrated;
58
+
59
+ if (!shouldHydrate) {
60
+ this.root.innerHTML = '';
61
+ }
62
+
63
+ // Instantiate and mount the page component with Params
64
+ const page = new ComponentClass({ params });
65
+ page.mount(this.root);
66
+
67
+ this.hydrated = true;
68
+ };
69
+
70
+ // Check if we have the module loaded (client-side mapping)
71
+ // For file-based routing, `route.importPath` is server-side relative.
72
+ // Client needs to know how to import it.
73
+ // We assume `window.__ROUTES__` contains `importFn` or we Map it in `app.js`.
74
+ // Actually, passing functions in JSON (window.__ROUTES__) is impossible.
75
+ // So `app.js` must construct the Router with a mapping of `key -> import()`.
76
+
77
+ // If `routeMap` passed to constructor has a `loader` property (function), use it.
78
+ if (matchedRoute.loader) {
79
+ if (!this.hydrated && !this.root.hasChildNodes()) {
80
+ this.root.innerHTML = '<div>Loading route...</div>';
81
+ }
82
+
83
+ matchedRoute.loader().then(module => {
84
+ const Comp = module.default || module;
85
+ handleComponent(Comp);
86
+ }).catch(err => {
87
+ console.error('Route Loading Error', err);
88
+ this.root.innerHTML = '<h1>Error Loading Page</h1>';
89
+ });
90
+ } else {
91
+ // Fallback or Error
92
+ console.error('Route found but no loader defined on client:', matchedKey);
93
+ this.root.innerHTML = '<h1>Error: Route Configuration Missing</h1>';
94
+ }
95
+
96
+ } else {
97
+ this.root.innerHTML = '<h1>404 - Page Not Found</h1>';
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Programmatic navigation
103
+ * @param {string} path
104
+ */
105
+ navigate(path) {
106
+ window.history.pushState({}, '', path);
107
+ this.resolve();
108
+ }
109
+ }
package/src/Store.js ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * zhinnx Core - Store
3
+ * A lightweight global state management system.
4
+ */
5
+
6
+ export class Store {
7
+ constructor(initialState = {}) {
8
+ this.listeners = new Set();
9
+
10
+ // Proxy allows us to detect changes automatically
11
+ this.state = new Proxy(initialState, {
12
+ set: (target, property, value) => {
13
+ target[property] = value;
14
+ this.notify();
15
+ return true;
16
+ }
17
+ });
18
+ }
19
+
20
+ /**
21
+ * Subscribe to state changes.
22
+ * @param {Function} listener - Function to call when state changes.
23
+ * @returns {Function} - Unsubscribe function.
24
+ */
25
+ subscribe(listener) {
26
+ this.listeners.add(listener);
27
+ // Return an unsubscribe function for cleanup
28
+ return () => this.listeners.delete(listener);
29
+ }
30
+
31
+ /**
32
+ * Notify all subscribers of a change.
33
+ */
34
+ notify() {
35
+ this.listeners.forEach(listener => listener(this.state));
36
+ }
37
+
38
+ /**
39
+ * Action helper to mutate state in a structured way (optional pattern)
40
+ * @param {Function} actionFn
41
+ */
42
+ dispatch(actionFn) {
43
+ actionFn(this.state);
44
+ }
45
+ }
package/src/diff.js ADDED
@@ -0,0 +1,284 @@
1
+
2
+ /**
3
+ * Zhinnx VDOM Diffing Engine
4
+ * Optimized for Fragments and Arrays
5
+ */
6
+
7
+ export function mount(vnode, container, anchor = null) {
8
+ if (vnode === null || vnode === undefined || vnode === false) return;
9
+
10
+ if (Array.isArray(vnode)) {
11
+ vnode.forEach(child => mount(child, container, anchor));
12
+ return; // Fragments don't return a single EL
13
+ }
14
+
15
+ const el = createDOM(vnode);
16
+ vnode.el = el;
17
+ container.insertBefore(el, anchor);
18
+ return el;
19
+ }
20
+
21
+ export function unmount(vnode) {
22
+ if (vnode === null || vnode === undefined || vnode === false) return;
23
+
24
+ if (Array.isArray(vnode)) {
25
+ vnode.forEach(unmount);
26
+ return;
27
+ }
28
+
29
+ if (vnode.el && vnode.el.parentNode) {
30
+ vnode.el.parentNode.removeChild(vnode.el);
31
+ }
32
+ }
33
+
34
+ function createDOM(vnode) {
35
+ if (vnode.text !== undefined) {
36
+ return document.createTextNode(vnode.text);
37
+ }
38
+ const el = document.createElement(vnode.tag);
39
+ if (vnode.props) {
40
+ for (const key in vnode.props) {
41
+ patchProp(el, key, null, vnode.props[key]);
42
+ }
43
+ }
44
+ if (vnode.children) {
45
+ vnode.children.forEach(child => mount(child, el));
46
+ }
47
+ return el;
48
+ }
49
+
50
+ /**
51
+ * Hydrates a VNode into an existing DOM Node.
52
+ */
53
+ export function hydrate(vnode, container) {
54
+ let domNode = container.firstChild;
55
+
56
+ // Normalize vnode to array for consistent handling
57
+ const nodes = Array.isArray(vnode) ? vnode : [vnode];
58
+
59
+ nodes.forEach(child => {
60
+ domNode = hydrateNode(child, domNode);
61
+ });
62
+ }
63
+
64
+ function hydrateNode(vnode, domNode) {
65
+ if (vnode === null || vnode === undefined || vnode === false) return domNode;
66
+
67
+ if (Array.isArray(vnode)) {
68
+ vnode.forEach(child => {
69
+ domNode = hydrateNode(child, domNode);
70
+ });
71
+ return domNode;
72
+ }
73
+
74
+ if (!domNode) {
75
+ mount(vnode, domNode ? domNode.parentNode : null);
76
+ return null;
77
+ }
78
+
79
+ vnode.el = domNode;
80
+
81
+ // Text Node
82
+ if (vnode.text !== undefined) {
83
+ if (domNode.nodeType !== Node.TEXT_NODE) {
84
+ // console.warn('Hydration Mismatch: Expected Text, found Element');
85
+ const newEl = createDOM(vnode);
86
+ domNode.parentNode.replaceChild(newEl, domNode);
87
+ vnode.el = newEl;
88
+ return newEl.nextSibling;
89
+ }
90
+ if (domNode.nodeValue !== vnode.text) {
91
+ domNode.nodeValue = vnode.text;
92
+ }
93
+ return domNode.nextSibling;
94
+ }
95
+
96
+ // Element Node
97
+ if (domNode.nodeType !== Node.ELEMENT_NODE || domNode.tagName.toLowerCase() !== vnode.tag.toLowerCase()) {
98
+ // console.warn(`Hydration Mismatch: Expected <${vnode.tag}>, found <${domNode.tagName}>`);
99
+ const newEl = createDOM(vnode);
100
+ domNode.parentNode.replaceChild(newEl, domNode);
101
+ vnode.el = newEl;
102
+ return newEl.nextSibling;
103
+ }
104
+
105
+ // Patch Props
106
+ if (vnode.props) {
107
+ for (const key in vnode.props) {
108
+ if (key.startsWith('on')) {
109
+ patchProp(domNode, key, null, vnode.props[key]);
110
+ }
111
+ }
112
+ }
113
+
114
+ // Hydrate Children
115
+ let childDomNode = domNode.firstChild;
116
+ if (vnode.children) {
117
+ vnode.children.forEach(child => {
118
+ childDomNode = hydrateNode(child, childDomNode);
119
+ });
120
+ }
121
+
122
+ return domNode.nextSibling;
123
+ }
124
+
125
+
126
+ export function patch(n1, n2, container) {
127
+ if (n1 === n2) return;
128
+
129
+ // Handle Arrays (Fragments)
130
+ if (Array.isArray(n1) || Array.isArray(n2)) {
131
+ const c1 = Array.isArray(n1) ? n1 : [n1];
132
+ const c2 = Array.isArray(n2) ? n2 : [n2];
133
+ diffChildren(c1, c2, container);
134
+ return;
135
+ }
136
+
137
+ // Replace if different types
138
+ if (n1.tag !== n2.tag || (n1.text !== undefined && n2.text === undefined) || (n1.text === undefined && n2.text !== undefined)) {
139
+ const anchor = n1.el.nextSibling;
140
+ unmount(n1);
141
+ mount(n2, container, anchor);
142
+ return;
143
+ }
144
+
145
+ // Text Update
146
+ if (n2.text !== undefined) {
147
+ n2.el = n1.el;
148
+ if (n2.text !== n1.text) {
149
+ n2.el.nodeValue = n2.text;
150
+ }
151
+ return;
152
+ }
153
+
154
+ // Element Update
155
+ const el = (n2.el = n1.el);
156
+
157
+ // Patch Props
158
+ const oldProps = n1.props || {};
159
+ const newProps = n2.props || {};
160
+ for (const key in newProps) {
161
+ const oldValue = oldProps[key];
162
+ const newValue = newProps[key];
163
+ if (newValue !== oldValue) {
164
+ patchProp(el, key, oldValue, newValue);
165
+ }
166
+ }
167
+ for (const key in oldProps) {
168
+ if (!(key in newProps)) {
169
+ patchProp(el, key, oldProps[key], null);
170
+ }
171
+ }
172
+
173
+ // Patch Children
174
+ diffChildren(n1.children, n2.children, el);
175
+ }
176
+
177
+ export function diffChildren(oldChildren, newChildren, container) {
178
+ // Optimization: fast path for empty
179
+ if (oldChildren.length === 0) {
180
+ newChildren.forEach(c => mount(c, container));
181
+ return;
182
+ }
183
+ if (newChildren.length === 0) {
184
+ oldChildren.forEach(c => unmount(c));
185
+ return;
186
+ }
187
+
188
+ const oldMap = new Map();
189
+ const unkeyed = [];
190
+
191
+ oldChildren.forEach((c, i) => {
192
+ if (c.key != null) oldMap.set(c.key, c);
193
+ else unkeyed.push({ vnode: c, index: i });
194
+ });
195
+
196
+ let unkeyedIndex = 0;
197
+
198
+ // We need to track where we are inserting in the DOM.
199
+ // Since we are patching in place, we can use the old children's locations,
200
+ // but if we move things, it gets complex.
201
+ // For this implementation, we use `insertBefore` with a reference node.
202
+ // But getting the reference node is hard if the old list is shuffled.
203
+
204
+ // Simplified Reconciler:
205
+ // 1. Walk new children.
206
+ // 2. If matched (key or type), patch. Move if needed.
207
+ // 3. If new, mount.
208
+
209
+ // To properly place nodes, we can use `container.childNodes` but that includes text nodes and comments
210
+ // that might not be in VDOM if VDOM stripped them (vdom.js keeps text).
211
+
212
+ // We'll trust the VDOM order matches DOM order initially.
213
+
214
+ // Pointer to the *next* sibling for insertion
215
+ let nextSibling = oldChildren[0]?.el;
216
+
217
+ // Wait, this is getting into React Fiber complexity.
218
+ // Fallback to the previous implementation which was decent for keyed,
219
+ // but ensure we handle `mount` correctly.
220
+
221
+ // Let's use the previous implementation logic but fix the anchor.
222
+
223
+ const newChildrenSize = newChildren.length;
224
+
225
+ // First, process keyed updates and moves
226
+ for (let i = 0; i < newChildrenSize; i++) {
227
+ const newChild = newChildren[i];
228
+ let oldChild;
229
+
230
+ if (newChild.key != null) {
231
+ oldChild = oldMap.get(newChild.key);
232
+ oldMap.delete(newChild.key);
233
+ } else {
234
+ if (unkeyedIndex < unkeyed.length) {
235
+ oldChild = unkeyed[unkeyedIndex].vnode;
236
+ unkeyedIndex++;
237
+ }
238
+ }
239
+
240
+ if (oldChild) {
241
+ patch(oldChild, newChild, container);
242
+ // Move: check if the DOM node at this position is correct
243
+ // The DOM node at index `i` (ignoring non-vdom nodes? No, we assume 1-to-1)
244
+ // But if we have fragments, index `i` is meaningless.
245
+
246
+ // Critical fix: We assume Component doesn't return Fragments for now in diffChildren logic
247
+ // OR we accept that reordering Fragments is expensive/not supported.
248
+ // Component.render() returning Array is handled by `patch` -> `diffChildren` on container.
249
+
250
+ // Let's rely on `container.childNodes[i]` assuming 1-to-1 mapping for Elements/Text.
251
+ const currentNode = container.childNodes[i];
252
+ if (oldChild.el && currentNode !== oldChild.el) {
253
+ container.insertBefore(oldChild.el, currentNode);
254
+ }
255
+ } else {
256
+ // Mount new
257
+ // We want to insert it at index `i`.
258
+ const anchor = container.childNodes[i];
259
+ mount(newChild, container, anchor);
260
+ }
261
+ }
262
+
263
+ // Cleanup
264
+ oldMap.forEach(c => unmount(c));
265
+ while (unkeyedIndex < unkeyed.length) {
266
+ unmount(unkeyed[unkeyedIndex].vnode);
267
+ unkeyedIndex++;
268
+ }
269
+ }
270
+
271
+ function patchProp(el, key, prev, next) {
272
+ if (key.startsWith('on')) {
273
+ const name = key.slice(2).toLowerCase();
274
+ if (prev) el.removeEventListener(name, prev);
275
+ if (next) el.addEventListener(name, next);
276
+ } else if (key === 'value' || key === 'checked') {
277
+ el[key] = next;
278
+ } else if (key === 'className') {
279
+ el.className = next || '';
280
+ } else {
281
+ if (next == null || next === false) el.removeAttribute(key);
282
+ else el.setAttribute(key, next);
283
+ }
284
+ }
@@ -0,0 +1,142 @@
1
+ // Zhinnx v2 Reactivity System
2
+ // Optimized with dependency cleanup and lazy computed properties
3
+
4
+ let activeEffect = null;
5
+ const targetMap = new WeakMap();
6
+
7
+ /**
8
+ * Tracks dependencies for a reactive property.
9
+ * @param {Object} target
10
+ * @param {string} key
11
+ */
12
+ export function track(target, key) {
13
+ if (activeEffect) {
14
+ let depsMap = targetMap.get(target);
15
+ if (!depsMap) {
16
+ depsMap = new Map();
17
+ targetMap.set(target, depsMap);
18
+ }
19
+ let dep = depsMap.get(key);
20
+ if (!dep) {
21
+ dep = new Set();
22
+ depsMap.set(key, dep);
23
+ }
24
+ dep.add(activeEffect);
25
+ activeEffect.deps.push(dep);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Triggers effects associated with a reactive property.
31
+ * @param {Object} target
32
+ * @param {string} key
33
+ */
34
+ export function trigger(target, key) {
35
+ const depsMap = targetMap.get(target);
36
+ if (!depsMap) return;
37
+ const dep = depsMap.get(key);
38
+ if (dep) {
39
+ const effectsToRun = new Set(dep);
40
+ effectsToRun.forEach(eff => {
41
+ if (eff.scheduler) {
42
+ eff.scheduler();
43
+ } else {
44
+ eff();
45
+ }
46
+ });
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Creates a reactive proxy for an object.
52
+ * @param {Object} target
53
+ * @returns {Proxy}
54
+ */
55
+ export function reactive(target) {
56
+ return new Proxy(target, {
57
+ get(obj, prop) {
58
+ track(obj, prop);
59
+ return Reflect.get(obj, prop);
60
+ },
61
+ set(obj, prop, value) {
62
+ const oldValue = obj[prop];
63
+ const result = Reflect.set(obj, prop, value);
64
+ if (oldValue !== value) {
65
+ trigger(obj, prop);
66
+ }
67
+ return result;
68
+ }
69
+ });
70
+ }
71
+
72
+ function cleanupEffect(effectFn) {
73
+ const { deps } = effectFn;
74
+ if (deps.length) {
75
+ for (let i = 0; i < deps.length; i++) {
76
+ deps[i].delete(effectFn);
77
+ }
78
+ deps.length = 0;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Registers a side effect that runs when dependencies change.
84
+ * @param {Function} fn - The function to run
85
+ * @param {Object} options - { lazy: boolean, scheduler: Function }
86
+ */
87
+ export function effect(fn, options = {}) {
88
+ const effectFn = () => {
89
+ // cleanup previous dependencies to avoid memory leaks and stale deps
90
+ cleanupEffect(effectFn);
91
+
92
+ const previousEffect = activeEffect;
93
+ activeEffect = effectFn;
94
+ try {
95
+ return fn();
96
+ } finally {
97
+ activeEffect = previousEffect;
98
+ }
99
+ };
100
+
101
+ effectFn.deps = [];
102
+ effectFn.scheduler = options.scheduler;
103
+
104
+ if (!options.lazy) {
105
+ effectFn();
106
+ }
107
+
108
+ return effectFn;
109
+ }
110
+
111
+ /**
112
+ * Creates a computed property.
113
+ * @param {Function} getter
114
+ * @returns {Object} { value }
115
+ */
116
+ export function computed(getter) {
117
+ let value;
118
+ let dirty = true;
119
+
120
+ const effectFn = effect(getter, {
121
+ lazy: true,
122
+ scheduler: () => {
123
+ if (!dirty) {
124
+ dirty = true;
125
+ trigger(computedObj, 'value');
126
+ }
127
+ }
128
+ });
129
+
130
+ const computedObj = {
131
+ get value() {
132
+ if (dirty) {
133
+ value = effectFn();
134
+ dirty = false;
135
+ }
136
+ track(computedObj, 'value');
137
+ return value;
138
+ }
139
+ };
140
+
141
+ return computedObj;
142
+ }
package/src/vdom.js ADDED
@@ -0,0 +1,118 @@
1
+
2
+ /**
3
+ * zhinnx VDOM
4
+ * Lightweight Virtual DOM implementation.
5
+ */
6
+
7
+ // VNode Factory
8
+ export function h(tag, props, ...children) {
9
+ return {
10
+ tag,
11
+ props: props || {},
12
+ children: children.flat(Infinity)
13
+ .filter(c => c != null && c !== false)
14
+ .map(c => (typeof c === 'string' || typeof c === 'number') ? { text: String(c) } : c),
15
+ key: props?.key
16
+ };
17
+ }
18
+
19
+ // Token to identify dynamic values in HTML
20
+ const UID = '__ZHIN_VAL__';
21
+
22
+ /**
23
+ * Tagged template function to create VNodes.
24
+ * Uses the browser's native HTML parser (via <template>).
25
+ * Note: This implementation requires a browser environment (DOM).
26
+ */
27
+ export function html(strings, ...values) {
28
+ // SSR Check - Return SSR Object
29
+ if (typeof document === 'undefined' || (typeof global !== 'undefined' && global.IS_SSR_TEST)) {
30
+ return {
31
+ isSSR: true,
32
+ strings,
33
+ values
34
+ };
35
+ }
36
+
37
+ // 1. Interleave strings and placeholders
38
+ let htmlString = '';
39
+ const valMap = new Map();
40
+
41
+ strings.forEach((str, i) => {
42
+ htmlString += str;
43
+ if (i < values.length) {
44
+ const key = UID + i;
45
+ valMap.set(key, values[i]);
46
+ htmlString += key;
47
+ }
48
+ });
49
+
50
+ const template = document.createElement('template');
51
+ template.innerHTML = htmlString;
52
+
53
+ // 3. Convert DOM to VNodes
54
+ return walk(template.content, valMap);
55
+ }
56
+
57
+ function walk(node, valMap) {
58
+ if (node.nodeType === Node.TEXT_NODE) {
59
+ const text = node.textContent;
60
+ // Check if the text is exactly a placeholder
61
+ // Note: This logic assumes placeholders are the entire text content or properly separated.
62
+ // For mixed text "Hello ${name}", the parser might see "Hello __ZHIN_VAL__0".
63
+ // We need to split.
64
+
65
+ if (text.includes(UID)) {
66
+ // Split by UID regex
67
+ const parts = text.split(new RegExp(`(${UID}\\d+)`));
68
+ const nodes = parts.map(part => {
69
+ if (part.startsWith(UID)) {
70
+ return valMap.get(part);
71
+ } else if (part) {
72
+ return { text: part }; // plain text normalized to VNode
73
+ }
74
+ return null;
75
+ }).filter(n => n !== null);
76
+
77
+ // If single item, return it
78
+ if (nodes.length === 1) return nodes[0];
79
+ return nodes;
80
+ }
81
+ return { text };
82
+ }
83
+
84
+ if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
85
+ // If it's a fragment, just return children
86
+ if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
87
+ let children = Array.from(node.childNodes).map(c => walk(c, valMap)).flat();
88
+ // Normalize strings if any leaked (though walk should handle them now)
89
+ children = children.map(c => (typeof c === 'string') ? { text: c } : c);
90
+
91
+ // If the fragment results in a single VNode, return it.
92
+ if (children.length === 1) return children[0];
93
+ return children;
94
+ }
95
+
96
+ const tag = node.tagName.toLowerCase();
97
+ const props = {};
98
+
99
+ // Attributes
100
+ Array.from(node.attributes).forEach(attr => {
101
+ let name = attr.name;
102
+ let value = attr.value;
103
+
104
+ // Check if value is a placeholder
105
+ if (value.startsWith(UID) && valMap.has(value)) {
106
+ value = valMap.get(value);
107
+ }
108
+
109
+ props[name] = value;
110
+ });
111
+
112
+ const children = Array.from(node.childNodes).map(c => walk(c, valMap)).flat();
113
+
114
+ return h(tag, props, ...children);
115
+ }
116
+
117
+ return null;
118
+ }