@faber1999/axon.js 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Faber Grajales Hincapié
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,292 @@
1
+ # axon.js
2
+
3
+ A fine-grained reactive frontend framework built from scratch.
4
+ JSX syntax · Signals reactivity · Router · Store · No Virtual DOM · Zero dependencies.
5
+
6
+ ```tsx
7
+ import { signal, createApp } from 'axon.js'
8
+
9
+ function Counter() {
10
+ const [count, setCount] = signal(0)
11
+
12
+ return (
13
+ <div>
14
+ <p>Count: {count}</p>
15
+ <button onClick={() => setCount((c) => c + 1)}>+1</button>
16
+ </div>
17
+ )
18
+ }
19
+
20
+ createApp(Counter).mount('#app')
21
+ ```
22
+
23
+ Components run **once**. Only the exact DOM nodes that depend on a signal update — no diffing, no re-renders.
24
+
25
+ ---
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ npm install axon.js
31
+ ```
32
+
33
+ ### Vite setup
34
+
35
+ **`vite.config.ts`**
36
+
37
+ ```ts
38
+ import { defineConfig } from 'vite'
39
+
40
+ export default defineConfig({
41
+ esbuild: {
42
+ jsxFactory: 'h',
43
+ jsxFragment: 'Fragment',
44
+ jsxInject: `import { h, Fragment } from 'axon.js/jsx'`
45
+ }
46
+ })
47
+ ```
48
+
49
+ **`tsconfig.json`**
50
+
51
+ ```json
52
+ {
53
+ "compilerOptions": {
54
+ "jsx": "preserve",
55
+ "jsxFactory": "h",
56
+ "jsxFragmentFactory": "Fragment",
57
+ "strict": true
58
+ }
59
+ }
60
+ ```
61
+
62
+ That's it. No plugins, no Babel, no extra config.
63
+
64
+ ---
65
+
66
+ ## Core API
67
+
68
+ ### Reactivity
69
+
70
+ ```ts
71
+ import { signal, effect, computed, batch, untrack } from 'axon.js'
72
+
73
+ // signal — reactive value
74
+ const [count, setCount] = signal(0)
75
+ count() // read
76
+ setCount(1) // write
77
+ setCount((c) => c + 1) // update with function
78
+
79
+ // effect — runs immediately and re-runs when dependencies change
80
+ effect(() => {
81
+ console.log('count is', count())
82
+ })
83
+
84
+ // computed — derived reactive value
85
+ const double = computed(() => count() * 2)
86
+ double() // 0, 2, 4...
87
+
88
+ // batch — group multiple updates into one notification
89
+ batch(() => {
90
+ setFirstName('John')
91
+ setLastName('Doe')
92
+ })
93
+
94
+ // untrack — read a signal without subscribing
95
+ effect(() => {
96
+ const a = count() // subscribes
97
+ const b = untrack(() => x()) // does NOT subscribe
98
+ })
99
+ ```
100
+
101
+ ### JSX & Components
102
+
103
+ ```tsx
104
+ import { onMount, onCleanup, createApp } from 'axon.js'
105
+
106
+ function Timer() {
107
+ const [seconds, setSeconds] = signal(0)
108
+
109
+ onMount(() => {
110
+ const id = setInterval(() => setSeconds((s) => s + 1), 1000)
111
+ onCleanup(() => clearInterval(id))
112
+ })
113
+
114
+ return <p>Elapsed: {seconds}s</p>
115
+ }
116
+
117
+ createApp(Timer).mount('#app')
118
+ ```
119
+
120
+ ### Control flow
121
+
122
+ ```tsx
123
+ import { Show, For, Dynamic, Portal } from 'axon.js'
124
+
125
+ // Conditional rendering
126
+ <Show when={isLoggedIn} fallback={<Login />}>
127
+ <Dashboard />
128
+ </Show>
129
+
130
+ // List rendering
131
+ <For each={todos}>
132
+ {(todo, index) => <li>{todo.text}</li>}
133
+ </For>
134
+
135
+ // Dynamic component
136
+ <Dynamic component={currentView} />
137
+
138
+ // Render outside the tree (e.g. modals)
139
+ <Portal mount={document.body}>
140
+ <Modal />
141
+ </Portal>
142
+ ```
143
+
144
+ ### Router
145
+
146
+ ```tsx
147
+ import { createRouter, RouterView, Link, useRouter, useParams } from 'axon.js'
148
+
149
+ createRouter(
150
+ [
151
+ { path: '/', component: Home },
152
+ { path: '/about', component: About },
153
+
154
+ // Route groups — shared layout and/or guard
155
+ {
156
+ layout: DashboardLayout,
157
+ guard: () => isLoggedIn() || '/login',
158
+ fallbackPath: '/login',
159
+ children: [
160
+ { path: '/dashboard', component: Dashboard },
161
+ { path: '/settings', component: Settings }
162
+ ]
163
+ },
164
+
165
+ // Catch-all 404
166
+ { path: '*', component: NotFound }
167
+ ],
168
+ { viewTransitions: true }
169
+ ) // optional animated transitions
170
+
171
+ function App() {
172
+ return (
173
+ <div>
174
+ <nav>
175
+ <Link href="/">Home</Link>
176
+ <Link href="/about" activeClass="active">
177
+ About
178
+ </Link>
179
+ </nav>
180
+ <main>
181
+ <RouterView />
182
+ </main>
183
+ </div>
184
+ )
185
+ }
186
+ ```
187
+
188
+ **Guard return values:**
189
+
190
+ | Returns | Behavior |
191
+ | --------- | --------------------------------------------------------- |
192
+ | `true` | Allow access, render the component |
193
+ | `false` | Navigate back to the previous route, or to `fallbackPath` |
194
+ | `"/path"` | Redirect to that path |
195
+
196
+ **Router hooks:**
197
+
198
+ ```ts
199
+ const router = useRouter() // full router instance
200
+ const params = useParams() // { id: '42' }
201
+ const navigate = useNavigate() // navigate('/path')
202
+ ```
203
+
204
+ ### Store
205
+
206
+ ```ts
207
+ import { createStore, select } from 'axon.js'
208
+
209
+ interface AppState {
210
+ theme: 'dark' | 'light'
211
+ count: number
212
+ }
213
+
214
+ const [store, setStore] = createStore<AppState>({
215
+ theme: 'dark',
216
+ count: 0
217
+ })
218
+
219
+ store.theme // read (reactive)
220
+ setStore('theme', 'light') // set one property
221
+ setStore('count', (c) => c + 1) // update with function
222
+ setStore({ theme: 'light', count: 5 }) // merge update
223
+
224
+ // Derived value from store
225
+ const label = select(store, (s) => `Theme: ${s.theme}`)
226
+ label() // reactive getter
227
+ ```
228
+
229
+ Multiple independent stores are supported — just call `createStore` multiple times.
230
+
231
+ ### Context
232
+
233
+ ```tsx
234
+ import { createContext } from 'axon.js'
235
+
236
+ const ThemeContext = createContext<'dark' | 'light'>('dark')
237
+
238
+ function App() {
239
+ return (
240
+ <ThemeContext.Provider value="light">
241
+ <Page />
242
+ </ThemeContext.Provider>
243
+ )
244
+ }
245
+
246
+ function Page() {
247
+ const theme = ThemeContext.use()
248
+ return <div class={theme}>...</div>
249
+ }
250
+ ```
251
+
252
+ ### View Transitions
253
+
254
+ Pass `{ viewTransitions: true }` to `createRouter` and name your content area:
255
+
256
+ ```css
257
+ main {
258
+ view-transition-name: page;
259
+ }
260
+ ::view-transition-old(page) {
261
+ animation: 120ms ease-out fade-out both;
262
+ }
263
+ ::view-transition-new(page) {
264
+ animation: 180ms ease-in fade-in both;
265
+ }
266
+ ```
267
+
268
+ The framework handles the rest automatically. Falls back gracefully in unsupported browsers.
269
+
270
+ ---
271
+
272
+ ## How it works
273
+
274
+ axon.js uses **fine-grained reactivity**: a global effect stack tracks which signals are read during execution, creating subscriptions automatically. No compiler magic — pure runtime JavaScript.
275
+
276
+ - Components execute **once** to build their initial DOM.
277
+ - Only `effect()` callbacks re-execute when signals change.
278
+ - DOM updates are surgical — only the exact node that depends on a signal is touched.
279
+
280
+ For a deep dive into the internals, see [INTERNALS.md](INTERNALS.md).
281
+
282
+ ---
283
+
284
+ ## License
285
+
286
+ MIT
287
+
288
+ ---
289
+
290
+ <div align="center">
291
+ <sub>Built with ❤️ by <a href="https://github.com/faber1999">faber1999</a></sub>
292
+ </div>
@@ -0,0 +1,227 @@
1
+ // src/reactivity/effect.ts
2
+ var effectStack = [];
3
+ function getCurrentEffect() {
4
+ return effectStack[effectStack.length - 1] ?? null;
5
+ }
6
+ function effect(fn) {
7
+ let cleanup = null;
8
+ const run = (() => {
9
+ if (typeof cleanup === "function") {
10
+ cleanup();
11
+ cleanup = null;
12
+ }
13
+ effectStack.push(run);
14
+ try {
15
+ cleanup = fn() ?? null;
16
+ } finally {
17
+ effectStack.pop();
18
+ }
19
+ });
20
+ run._subscriptions = /* @__PURE__ */ new Set();
21
+ run._disposed = false;
22
+ run();
23
+ const dispose = () => {
24
+ run._disposed = true;
25
+ run._subscriptions.forEach((unsub) => unsub());
26
+ run._subscriptions.clear();
27
+ if (typeof cleanup === "function") {
28
+ cleanup();
29
+ cleanup = null;
30
+ }
31
+ };
32
+ return dispose;
33
+ }
34
+ function untrack(fn) {
35
+ const saved = effectStack.splice(0);
36
+ try {
37
+ return fn();
38
+ } finally {
39
+ effectStack.push(...saved);
40
+ }
41
+ }
42
+
43
+ // src/component/lifecycle.ts
44
+ var ownerStack = [];
45
+ function getCurrentOwner() {
46
+ return ownerStack[ownerStack.length - 1] ?? null;
47
+ }
48
+ function runOwned(fn) {
49
+ const owner = {
50
+ _onMount: [],
51
+ _onCleanup: [],
52
+ _children: [],
53
+ _mounted: false
54
+ };
55
+ const parent = getCurrentOwner();
56
+ if (parent) parent._children.push(owner);
57
+ ownerStack.push(owner);
58
+ let result;
59
+ try {
60
+ result = fn();
61
+ } finally {
62
+ ownerStack.pop();
63
+ }
64
+ queueMicrotask(() => {
65
+ if (!owner._mounted) {
66
+ owner._mounted = true;
67
+ owner._onMount.forEach((cb) => cb());
68
+ }
69
+ });
70
+ return [result, owner];
71
+ }
72
+ function runWithOwner(fn, props) {
73
+ const [result] = runOwned(() => fn(props));
74
+ return result;
75
+ }
76
+ function onMount(fn) {
77
+ const owner = getCurrentOwner();
78
+ if (owner) owner._onMount.push(fn);
79
+ else console.warn("[axon] onMount called outside of a component");
80
+ }
81
+ function onCleanup(fn) {
82
+ const owner = getCurrentOwner();
83
+ if (owner) owner._onCleanup.push(fn);
84
+ else console.warn("[axon] onCleanup called outside of a component");
85
+ }
86
+ function disposeOwner(owner) {
87
+ owner._children.forEach(disposeOwner);
88
+ owner._onCleanup.forEach((cb) => cb());
89
+ owner._onMount = [];
90
+ owner._onCleanup = [];
91
+ owner._children = [];
92
+ }
93
+
94
+ // src/dom/h.ts
95
+ var Fragment = /* @__PURE__ */ Symbol("Fragment");
96
+ var BOOLEAN_ATTRS = /* @__PURE__ */ new Set([
97
+ "checked",
98
+ "disabled",
99
+ "readonly",
100
+ "multiple",
101
+ "selected",
102
+ "autofocus",
103
+ "autoplay",
104
+ "controls",
105
+ "default",
106
+ "defer",
107
+ "formnovalidate",
108
+ "hidden",
109
+ "ismap",
110
+ "loop",
111
+ "novalidate",
112
+ "open",
113
+ "required",
114
+ "reversed",
115
+ "scoped",
116
+ "seamless"
117
+ ]);
118
+ function applyProp(el, key, value) {
119
+ if (key === "class" || key === "className") {
120
+ el.className = value ?? "";
121
+ } else if (key === "style") {
122
+ if (typeof value === "string") {
123
+ el.style.cssText = value;
124
+ } else if (value && typeof value === "object") {
125
+ Object.assign(el.style, value);
126
+ }
127
+ } else if (key === "ref") {
128
+ if (typeof value === "function") {
129
+ value(el);
130
+ } else if (value && typeof value === "object") {
131
+ value.current = el;
132
+ }
133
+ } else if (key.startsWith("on") && key.length > 2) {
134
+ const event = key.slice(2).toLowerCase();
135
+ el.addEventListener(event, value);
136
+ } else if (BOOLEAN_ATTRS.has(key)) {
137
+ if (value) el.setAttribute(key, "");
138
+ else el.removeAttribute(key);
139
+ } else if (key === "innerHTML") {
140
+ el.innerHTML = value;
141
+ } else {
142
+ if (value == null || value === false) {
143
+ el.removeAttribute(key);
144
+ } else {
145
+ el.setAttribute(key, value === true ? "" : String(value));
146
+ }
147
+ }
148
+ }
149
+ function toNode(value) {
150
+ if (value == null || value === false) return document.createTextNode("");
151
+ if (value instanceof Node) return value;
152
+ if (typeof value === "function") return document.createTextNode("");
153
+ return document.createTextNode(String(value));
154
+ }
155
+ function appendChild(parent, child) {
156
+ if (child == null || child === false) return;
157
+ if (Array.isArray(child)) {
158
+ child.forEach((c) => appendChild(parent, c));
159
+ return;
160
+ }
161
+ if (child instanceof Node) {
162
+ parent.appendChild(child);
163
+ return;
164
+ }
165
+ if (typeof child === "function") {
166
+ const startMarker = document.createComment("");
167
+ const endMarker = document.createComment("");
168
+ parent.appendChild(startMarker);
169
+ parent.appendChild(endMarker);
170
+ effect(() => {
171
+ const result = child();
172
+ let node = startMarker.nextSibling;
173
+ while (node && node !== endMarker) {
174
+ const next = node.nextSibling;
175
+ parent.removeChild(node);
176
+ node = next;
177
+ }
178
+ const nodes = Array.isArray(result) ? result : [result];
179
+ nodes.forEach((n) => {
180
+ if (n != null && n !== false) {
181
+ parent.insertBefore(toNode(n), endMarker);
182
+ }
183
+ });
184
+ });
185
+ return;
186
+ }
187
+ parent.appendChild(document.createTextNode(String(child)));
188
+ }
189
+ function h(type, props, ...children) {
190
+ if (type === Fragment) {
191
+ return children.flat();
192
+ }
193
+ if (typeof type === "function") {
194
+ const componentProps = { ...props ?? {} };
195
+ if (children.length === 1) componentProps.children = children[0];
196
+ else if (children.length > 1) componentProps.children = children.flat();
197
+ return runWithOwner(type, componentProps);
198
+ }
199
+ const el = document.createElement(type);
200
+ if (props) {
201
+ for (const key of Object.keys(props)) {
202
+ const value = props[key];
203
+ if (key === "children") continue;
204
+ if (typeof value === "function" && !key.startsWith("on")) {
205
+ effect(() => applyProp(el, key, value()));
206
+ } else {
207
+ applyProp(el, key, value);
208
+ }
209
+ }
210
+ }
211
+ const flatChildren = children.flat();
212
+ flatChildren.forEach((child) => appendChild(el, child));
213
+ return el;
214
+ }
215
+
216
+ export {
217
+ getCurrentEffect,
218
+ effect,
219
+ untrack,
220
+ runOwned,
221
+ runWithOwner,
222
+ onMount,
223
+ onCleanup,
224
+ disposeOwner,
225
+ Fragment,
226
+ h
227
+ };