ajo 0.1.30 → 0.1.31

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/LLMs.md ADDED
@@ -0,0 +1,329 @@
1
+ # Ajo LLM Instructions
2
+
3
+ Ajo is a micro UI library using JSX and generators. **No React imports**: JSX compiles to Ajo via build config.
4
+
5
+ ## Stateless Component
6
+
7
+ ```tsx
8
+ import type { Stateless, WithChildren } from 'ajo'
9
+ import clsx from 'clsx' // optional, for conditional classes
10
+
11
+ type Args = WithChildren<{ title: string; active?: boolean }>
12
+
13
+ const Card: Stateless<Args> = ({ title, active, children }) => ( // can destructure args here
14
+ <div class={clsx('card', { active })}>
15
+ <h3>{title}</h3>
16
+ {children}
17
+ </div>
18
+ )
19
+
20
+ // Example usage - everything goes to args:
21
+ <Card title="Hello" active>
22
+ <p>This is a card.</p>
23
+ </Card>
24
+ ```
25
+
26
+ ## Stateful Component
27
+
28
+ ```tsx
29
+ import type { Stateful } from 'ajo'
30
+
31
+ type Args = { initial: number; step?: number }
32
+
33
+ const Counter: Stateful<Args, 'section'> = function* (args) { // do NOT destructure args here
34
+
35
+ // before loop: persistent state & handlers
36
+
37
+ let count = args.initial
38
+ let inputRef: HTMLInputElement | null = null
39
+
40
+ const inc = () => this.next(({ step = 1 }) => count += step)
41
+ const dec = () => this.next(() => count--)
42
+
43
+ const handleKeydown = (e: KeyboardEvent) => {
44
+ if (e.key === 'ArrowUp') inc()
45
+ else if (e.key === 'ArrowDown' && count > 0) dec()
46
+ }
47
+
48
+ this.addEventListener('keydown', handleKeydown, { signal: this.signal }) // auto-cleanup on unmount
49
+
50
+ while (true) { // main render loop
51
+
52
+ try { // optional: error boundary
53
+
54
+ // fresh destructure each render
55
+ const { step = 1 } = args
56
+
57
+ // derived values
58
+ const isEven = count % 2 === 0
59
+
60
+ yield (
61
+ <>
62
+ <input
63
+ ref={el => inputRef = el}
64
+ value={count}
65
+ set:oninput={e => this.next(() => count = +(e.target as HTMLInputElement).value)}
66
+ />
67
+ <button set:onclick={inc}>+{step}</button>
68
+ <button set:onclick={dec} disabled={count <= 0}>-</button>
69
+ <p memo={isEven}>Even: {isEven ? 'yes' : 'no'}</p>
70
+ <footer memo>Static content - rendered once</footer>
71
+ <div skip>{/* third-party managed DOM here */}</div>
72
+ </>
73
+ )
74
+ } catch (err: unknown) {
75
+ yield <p class="error">{err instanceof Error ? err.message : String(err)}</p>
76
+ }
77
+ }
78
+ }
79
+
80
+ Counter.is = 'section' // wrapper element (default: div)
81
+ Counter.attrs = { class: 'counter-wrap' } // default wrapper attributes
82
+ Counter.args = { step: 1 } // default args
83
+
84
+ // Example usage - special attrs apply to wrapper, rest goes to args:
85
+
86
+ let ref: ThisParameterType<typeof Counter> | null = null
87
+
88
+ <Counter
89
+ initial={0} step={5} // → args
90
+ attr:id="main" attr:class="my-counter" // → wrapper attributes (HTML attrs)
91
+ set:onclick={fn} // → wrapper properties (DOM props)
92
+ key={id} // → wrapper key
93
+ memo={[id]} // → wrapper memo (array)
94
+ memo={id} // → wrapper memo (single value)
95
+ memo // → wrapper memo (render once)
96
+ ref={el => ref = el} // → wrapper ref (el is <section> + .next()/.throw()/.return())
97
+ />
98
+
99
+ ref?.next() // trigger re-render from outside
100
+ ```
101
+
102
+ ## Rules
103
+
104
+ | Topic | Rule |
105
+ |-------|------|
106
+ | **Elements** | Everything becomes HTML attributes. `set:prop` assigns DOM properties instead (`node[prop] = value`) |
107
+ | **Stateless** | Everything goes to `args`. Special attrs like `memo` must be applied to elements inside |
108
+ | **Stateful** | `key`, `memo`, `skip`, `ref`, `set:*` apply to implicit wrapper element. `attr:*` sets wrapper attributes. Rest goes to `args` |
109
+ | **Events** | `set:onclick`, `set:oninput`, etc. Never `onClick` |
110
+ | **Classes** | `class`, never `className`. Must be string, no object/array syntax. Use `clsx()` or template literals |
111
+ | **Styles** | `style` must be string (`style="color: red"`), not object. No special handling |
112
+ | **Args** | Never destructure in generator signature. Use `args` param |
113
+ | **Root JSX** | Use `<>...</>` in stateful to avoid double wrapper |
114
+ | **Re-render** | `this.next(fn?)` — returns `fn`'s result. Use `this.throw(e)` for explicit error routing |
115
+ | **Context** | `context<T>(fallback)` creates context. Stateless: read only. Stateful: read/write inside `while` loop |
116
+ | **Lists** | Always provide unique `key` on elements |
117
+ | **Refs** | `ref={el => ...}` on elements. Receives `null` on unmount. Stateful ref type: `ThisParameterType<typeof Component>` |
118
+ | **Memo** | `memo={[deps]}` array, `memo={value}` single, or just `memo` (never re-render). Skips subtree if unchanged |
119
+ | **Skip** | `skip` excludes children from reconciliation. Use for `set:textContent`/`set:innerHTML` or third-party maneged DOM |
120
+ | **Custom wrapper** | Set `.is = 'tagname'` AND TypeScript generic `Stateful<Args, 'tagname'>` for stateful components. Default is `div` (no need to set) |
121
+ | **Default attrs** | `.attrs = { class: '...' }` on stateful component generator function |
122
+ | **Default args** | `.args = { prop: value }` on stateful component generator function |
123
+ | **Cleanup** | `this.signal` for APIs that accept AbortSignal (fetch, addEventListener). `try/finally` for the rest |
124
+ | **Error recovery** | `try { ... } catch { yield error UI }` inside loop |
125
+ | **this** | Stateful wrapper element with `.signal`, `.next(fn?)` (returns `fn`'s result), `.throw()`, `.return()`. Type: `ThisParameterType<typeof Component>` |
126
+
127
+ ## Common Patterns
128
+
129
+ ```tsx
130
+ import type { Stateful, Stateless, WithChildren } from 'ajo'
131
+ import { context } from 'ajo/context'
132
+
133
+ // Async data loading
134
+ type LoaderArgs = { url: string }
135
+
136
+ const DataLoader: Stateful<LoaderArgs> = function* (args) {
137
+
138
+ let data: unknown = null
139
+ let error: Error | null = null
140
+
141
+ fetch(args.url, { signal: this.signal })
142
+ .then(r => r.json())
143
+ .then(d => this.next(() => data = d))
144
+ .catch(e => this.next(() => error = e))
145
+
146
+ while (true) yield (
147
+ <>
148
+ {error ? <p>Error: {error.message}</p>
149
+ : data ? <div>{JSON.stringify(data)}</div>
150
+ : <p>Loading...</p>}
151
+ </>
152
+ )
153
+ }
154
+
155
+ // List with keys
156
+ type Item = { id: string; text: string }
157
+
158
+ const List: Stateless<{ items: Item[] }> = ({ items }) => (
159
+ <ul>
160
+ {items.map(item => <li key={item.id}>{item.text}</li>)}
161
+ </ul>
162
+ )
163
+
164
+ // Conditional rendering
165
+ type ShowArgs = WithChildren<{ when: boolean }>
166
+
167
+ const Show: Stateless<ShowArgs> = ({ when, children }) => when ? children : null
168
+
169
+ // Context - create with fallback value
170
+ const ThemeContext = context<'light' | 'dark'>('light')
171
+ const UserContext = context<{ name: string } | null>(null)
172
+
173
+ // Stateless - read only (call without args)
174
+ const ThemedCard: Stateless<{ title: string }> = ({ title }) => {
175
+ const theme = ThemeContext() // reads current value
176
+ return <div class={`card theme-${theme}`}>{title}</div>
177
+ }
178
+
179
+ // Stateful - read/write inside while loop
180
+ const ThemeProvider: Stateful<WithChildren> = function* (args) {
181
+
182
+ let theme: 'light' | 'dark' = 'light'
183
+
184
+ const toggle = () => this.next(() => theme = theme === 'light' ? 'dark' : 'light')
185
+
186
+ while (true) {
187
+
188
+ ThemeContext(theme) // write: sets value for descendants
189
+ const user = UserContext() // read: gets value from ancestor
190
+
191
+ yield (
192
+ <>
193
+ <button set:onclick={toggle}>Theme: {theme}</button>
194
+ {user && <span>User: {user.name}</span>}
195
+ {args.children}
196
+ </>
197
+ )
198
+ }
199
+ }
200
+
201
+ // Ref typing for stateful components
202
+ let counterRef: ThisParameterType<typeof Counter> | null = null
203
+ <Counter ref={el => counterRef = el} initial={0} />
204
+ // counterRef is <section> element + .next(), .throw(), .return() methods
205
+ counterRef?.next() // trigger re-render from outside
206
+
207
+ // memo variations
208
+ <div memo={[a, b]}>{/* re-render when a or b changes */}</div>
209
+ <div memo={count}>{/* re-render when count changes (single value) */}</div>
210
+ <div memo>{/* render once, never update - good for static content */}</div>
211
+
212
+ // Boolean attributes
213
+ <input type="checkbox" checked disabled /> // checked="" disabled=""
214
+ <button disabled={false} /> // removes disabled attr
215
+
216
+ // Attributes vs Properties (HTML-first)
217
+ <input value="text" /> // HTML attribute: initial value only
218
+ <input set:value={text} /> // DOM property: syncs with state
219
+ <input type="checkbox" checked /> // HTML attribute: initial state
220
+ <input type="checkbox" set:checked={bool} /> // DOM property: syncs with state
221
+ <video set:currentTime={0} set:muted /> // DOM properties
222
+ <div set:textContent={str} skip /> // DOM property + skip (required!)
223
+ <div set:innerHTML={html} skip /> // DOM property + skip (required!)
224
+
225
+ // Post-render work (DOM is updated by the time the microtask runs)
226
+ const ScrollList: Stateful<{ items: Item[] }> = function* (args) {
227
+ let container: HTMLUListElement | null = null
228
+ while (true) {
229
+ queueMicrotask(() => container!.scrollTop = container!.scrollHeight)
230
+ yield (
231
+ <ul ref={el => container = el}>
232
+ {args.items.map(item => <li key={item.id}>{item.text}</li>)}
233
+ </ul>
234
+ )
235
+ }
236
+ }
237
+
238
+ // Third-party managed DOM
239
+ let map: MapLibrary | null = null
240
+ <div skip ref={el => el ? (map ??= new MapLibrary(el)) : map?.destroy()} /> // skip lets library control children
241
+ ```
242
+
243
+ ## Anti-patterns
244
+
245
+ ```tsx
246
+ // ❌ React patterns: NEVER use
247
+ import React from 'react'
248
+ className="..."
249
+ onClick={...}
250
+ useState, useEffect, useCallback
251
+
252
+ // ❌ class/style as object or array: no special handling in Ajo
253
+ <div class={{ active: isActive }} /> // won't work
254
+ <div class={['btn', 'primary']} /> // won't work
255
+ <div style={{ color: 'red' }} /> // won't work
256
+
257
+ // ✅ class/style must be strings
258
+ <div class={`btn ${isActive ? 'active' : ''}`} />
259
+ <div class={clsx('btn', { active: isActive })} /> // use clsx library
260
+ <div style="color: red; font-size: 14px" />
261
+ <div style={`color: ${color}`} />
262
+
263
+ // ❌ memo in args but not applied to element - does nothing
264
+ const Bad: Stateless<{ memo: unknown }> = ({ memo }) => (
265
+ <div>content</div> // memo arg ignored, no memoization
266
+ )
267
+
268
+ // ✅ Pass deps in args, apply to root element
269
+ const Good: Stateless<{ deps?: unknown }> = ({ deps }) => (
270
+ <div memo={deps}>content</div> // memoized when deps provided
271
+ )
272
+ // <Good deps={[id]} /> or <Good deps={data} />
273
+
274
+ // ❌ Destructure in signature - locks to initial values
275
+ function* Bad({ count }) { ... }
276
+
277
+ // ❌ Context read/write outside loop in stateful - stale values
278
+ function* Bad(args) {
279
+ const theme = ThemeContext() // frozen at mount
280
+ ThemeContext('dark') // only set once, not updated
281
+ while (true) yield ...
282
+ }
283
+
284
+ // ✅ Context read/write inside loop in stateful
285
+ function* Good(args) {
286
+ let theme = 'light'
287
+ while (true) {
288
+ ThemeContext(theme) // write: updated each render
289
+ const user = UserContext() // read: fresh value each render
290
+ yield ...
291
+ }
292
+ }
293
+
294
+ // ❌ Missing key in lists
295
+ {items.map(item => <li>{item}</li>)}
296
+
297
+ // ❌ Direct state mutation without next()
298
+ const inc = () => count++ // won't re-render
299
+
300
+ // ❌ set:textContent/innerHTML without skip: content gets cleared
301
+ <div set:innerHTML={html} />
302
+
303
+ // ✅ Correct
304
+ const inc = () => this.next(() => count++)
305
+ <div set:innerHTML={html} skip />
306
+ ```
307
+
308
+ ## Setup
309
+
310
+ ```bash
311
+ npm install ajo
312
+ pnpm add ajo
313
+ yarn add ajo
314
+ ```
315
+
316
+ Configure JSX factory (Vite example):
317
+
318
+ ```ts
319
+ // vite.config.ts
320
+ export default defineConfig({
321
+ esbuild: {
322
+ jsxFactory: 'h',
323
+ jsxFragment: 'Fragment',
324
+ jsxInject: `import { h, Fragment } from 'ajo'`,
325
+ },
326
+ })
327
+ ```
328
+
329
+ For other build systems: `jsxFactory: 'h'`, `jsxFragment: 'Fragment'`, auto-import `{ h, Fragment }` from `'ajo'`.
package/context.js ADDED
@@ -0,0 +1,18 @@
1
+ export const Context = Symbol.for('ajo.context')
2
+
3
+ export const context = (fallback, key = Symbol()) => function (...args) {
4
+
5
+ const self = this ?? component
6
+
7
+ return self
8
+ ? args.length
9
+ ? self[Context][key] = args[0]
10
+ : key in self[Context]
11
+ ? self[Context][key]
12
+ : fallback
13
+ : fallback
14
+ }
15
+
16
+ let component = null
17
+
18
+ export const current = (...args) => args.length ? (component = args[0]) : component
package/dist/html.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("./context.cjs"),m=new Set(["area","base","br","col","command","embed","hr","img","input","keygen","link","meta","param","source","track","wbr"]),b=Symbol.for("ajo.args"),y=e=>e.replace(/[&<>"']/g,r=>`&#${r.charCodeAt(0)};`),f=()=>{},p=e=>[...a(e)].join(""),a=function*(e){for(e of c(e))typeof e=="string"?yield y(e):yield*k(e)},k=function*({nodeName:e,children:r,...i}){let o="";for(const n in i)n.startsWith("set:")||i[n]==null||i[n]===!1||(i[n]===!0?o+=` ${n}`:o+=` ${n}="${y(String(i[n]))}"`);m.has(e)?yield`<${e}${o}>`:(yield`<${e}${o}>`,r!=null&&(yield*a(r)),yield`</${e}>`)},c=function*(e){if(e==null)return;const r=typeof e;if(r!="boolean")if(r=="string")yield e;else if(r=="number"||r=="bigint")yield String(e);else if(Symbol.iterator in e)for(e of e)yield*c(e);else"nodeName"in e?typeof e.nodeName=="function"?yield*$(e):yield d(e):yield String(e)},$=function*({nodeName:e,fallback:r=e.fallback,...i}){e.constructor.name=="GeneratorFunction"?yield S(e,i):yield*c(e(i))},S=(e,r)=>{const i={...e.attrs},o={...e.args};for(const t in r)t.startsWith("attr:")?i[t.slice(5)]=r[t]:t=="key"||t=="skip"||t=="memo"||t=="ref"||t.startsWith("set:")?i[t]=r[t]:o[t]=r[t];const n={[s.Context]:Object.create(s.current()?.[s.Context]??null),[b]:o,next:f,return:f,throw:t=>{throw t}},l=e.call(n,o),g=s.current();s.current(n);const u=t=>({...i,nodeName:e.is??"div",...d({children:t})});try{return u(l.next().value)}catch(t){return u(l.throw(t).value)}finally{l.return?.(),s.current(g)}},d=({key:e,skip:r,memo:i,ref:o,...n})=>{if("children"in n){const l=[...c(n.children)];l.length?n.children=l.length==1?l[0]:l:delete n.children}return n};exports.html=a;exports.render=p;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("./context.cjs"),m=new Set(["area","base","br","col","command","embed","hr","img","input","keygen","link","meta","param","source","track","wbr"]),p=Symbol.for("ajo.args"),d=e=>e.replace(/[&<>"']/g,r=>`&#${r.charCodeAt(0)};`),y=()=>{},k=e=>[...u(e)].join(""),u=function*(e){for(e of c(e))typeof e=="string"?yield d(e):yield*$(e)},$=function*({nodeName:e,children:r,...i}){let o="";for(const n in i)n.startsWith("set:")||i[n]==null||i[n]===!1||(i[n]===!0?o+=` ${n}`:o+=` ${n}="${d(String(i[n]))}"`);m.has(e)?yield`<${e}${o}>`:(yield`<${e}${o}>`,r!=null&&(yield*u(r)),yield`</${e}>`)},c=function*(e){if(e==null)return;const r=typeof e;if(r!="boolean")if(r=="string")yield e;else if(r=="number"||r=="bigint")yield String(e);else if(Symbol.iterator in e)for(e of e)yield*c(e);else"nodeName"in e?typeof e.nodeName=="function"?yield*S(e):yield g(e):yield String(e)},S=function*({nodeName:e,fallback:r=e.fallback,...i}){e.constructor.name=="GeneratorFunction"?yield w(e,i):yield*c(e(i))},w=(e,r)=>{const i={...e.attrs},o={...e.args};for(const t in r)t.startsWith("attr:")?i[t.slice(5)]=r[t]:t=="key"||t=="skip"||t=="memo"||t=="ref"||t.startsWith("set:")?i[t]=r[t]:o[t]=r[t];const n=new AbortController,l={[s.Context]:Object.create(s.current()?.[s.Context]??null),[p]:o,signal:n.signal,next:y,return:y,throw:t=>{throw t}},a=e.call(l,o),b=s.current();s.current(l);const f=t=>({...i,nodeName:e.is??"div",...g({children:t})});try{return f(a.next().value)}catch(t){return f(a.throw(t).value)}finally{a.return(),n.abort(),s.current(b)}},g=({key:e,skip:r,memo:i,ref:o,...n})=>{if("children"in n){const l=[...c(n.children)];l.length?n.children=l.length==1?l[0]:l:delete n.children}return n};exports.html=u;exports.render=k;
package/dist/html.js CHANGED
@@ -1,13 +1,13 @@
1
- import { Context as f, current as s } from "./context.js";
2
- const p = /* @__PURE__ */ new Set(["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]), b = /* @__PURE__ */ Symbol.for("ajo.args"), y = (e) => e.replace(/[&<>"']/g, (n) => `&#${n.charCodeAt(0)};`), u = () => {
3
- }, v = (e) => [...d(e)].join(""), d = function* (e) {
1
+ import { Context as u, current as s } from "./context.js";
2
+ const p = /* @__PURE__ */ new Set(["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]), k = /* @__PURE__ */ Symbol.for("ajo.args"), d = (e) => e.replace(/[&<>"']/g, (n) => `&#${n.charCodeAt(0)};`), y = () => {
3
+ }, x = (e) => [...g(e)].join(""), g = function* (e) {
4
4
  for (e of c(e))
5
- typeof e == "string" ? yield y(e) : yield* k(e);
6
- }, k = function* ({ nodeName: e, children: n, ...i }) {
5
+ typeof e == "string" ? yield d(e) : yield* $(e);
6
+ }, $ = function* ({ nodeName: e, children: n, ...i }) {
7
7
  let o = "";
8
8
  for (const r in i)
9
- r.startsWith("set:") || i[r] == null || i[r] === !1 || (i[r] === !0 ? o += ` ${r}` : o += ` ${r}="${y(String(i[r]))}"`);
10
- p.has(e) ? yield `<${e}${o}>` : (yield `<${e}${o}>`, n != null && (yield* d(n)), yield `</${e}>`);
9
+ r.startsWith("set:") || i[r] == null || i[r] === !1 || (i[r] === !0 ? o += ` ${r}` : o += ` ${r}="${d(String(i[r]))}"`);
10
+ p.has(e) ? yield `<${e}${o}>` : (yield `<${e}${o}>`, n != null && (yield* g(n)), yield `</${e}>`);
11
11
  }, c = function* (e) {
12
12
  if (e == null) return;
13
13
  const n = typeof e;
@@ -15,30 +15,31 @@ const p = /* @__PURE__ */ new Set(["area", "base", "br", "col", "command", "embe
15
15
  if (n == "string") yield e;
16
16
  else if (n == "number" || n == "bigint") yield String(e);
17
17
  else if (Symbol.iterator in e) for (e of e) yield* c(e);
18
- else "nodeName" in e ? typeof e.nodeName == "function" ? yield* $(e) : yield m(e) : yield String(e);
19
- }, $ = function* ({ nodeName: e, fallback: n = e.fallback, ...i }) {
20
- e.constructor.name == "GeneratorFunction" ? yield w(e, i) : yield* c(e(i));
21
- }, w = (e, n) => {
18
+ else "nodeName" in e ? typeof e.nodeName == "function" ? yield* w(e) : yield m(e) : yield String(e);
19
+ }, w = function* ({ nodeName: e, fallback: n = e.fallback, ...i }) {
20
+ e.constructor.name == "GeneratorFunction" ? yield S(e, i) : yield* c(e(i));
21
+ }, S = (e, n) => {
22
22
  const i = { ...e.attrs }, o = { ...e.args };
23
23
  for (const t in n)
24
24
  t.startsWith("attr:") ? i[t.slice(5)] = n[t] : t == "key" || t == "skip" || t == "memo" || t == "ref" || t.startsWith("set:") ? i[t] = n[t] : o[t] = n[t];
25
- const r = {
26
- [f]: Object.create(s()?.[f] ?? null),
27
- [b]: o,
28
- next: u,
29
- return: u,
25
+ const r = new AbortController(), l = {
26
+ [u]: Object.create(s()?.[u] ?? null),
27
+ [k]: o,
28
+ signal: r.signal,
29
+ next: y,
30
+ return: y,
30
31
  throw: (t) => {
31
32
  throw t;
32
33
  }
33
- }, l = e.call(r, o), g = s();
34
- s(r);
35
- const a = (t) => ({ ...i, nodeName: e.is ?? "div", ...m({ children: t }) });
34
+ }, a = e.call(l, o), b = s();
35
+ s(l);
36
+ const f = (t) => ({ ...i, nodeName: e.is ?? "div", ...m({ children: t }) });
36
37
  try {
37
- return a(l.next().value);
38
+ return f(a.next().value);
38
39
  } catch (t) {
39
- return a(l.throw(t).value);
40
+ return f(a.throw(t).value);
40
41
  } finally {
41
- l.return?.(), s(g);
42
+ a.return(), r.abort(), s(b);
42
43
  }
43
44
  }, m = ({ key: e, skip: n, memo: i, ref: o, ...r }) => {
44
45
  if ("children" in r) {
@@ -48,6 +49,6 @@ const p = /* @__PURE__ */ new Set(["area", "base", "br", "col", "command", "embe
48
49
  return r;
49
50
  };
50
51
  export {
51
- d as html,
52
- v as render
52
+ g as html,
53
+ x as render
53
54
  };
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o=require("./context.cjs"),y=Symbol.for("ajo.key"),j=Symbol.for("ajo.memo"),g=Symbol.for("ajo.ref"),A=Symbol.for("ajo.cache"),c=Symbol.for("ajo.generator"),a=Symbol.for("ajo.iterator"),b=Symbol.for("ajo.render"),l=Symbol.for("ajo.args"),N=e=>e.children,k=(e,t,...r)=>((t??={}).nodeName=e,!("children"in t)&&r.length&&(t.children=r.length==1?r[0]:r),t),f=(e,t,r=t.firstChild,s=null)=>{for(e of S(e)){const n=E(e,t,r);r==null?m(t,n,s):n==r?r=n.nextSibling:n==r.nextSibling?(m(t,r,s),r=n.nextSibling):m(t,n,r)}for(;r!=s;){const n=r.nextSibling;r.nodeType==1&&p(r),t.removeChild(r),r=n}},S=function*(e){if(e==null)return;const t=typeof e;if(t!="boolean")if(t=="string")yield e;else if(t=="number"||t=="bigint")yield String(e);else if(Symbol.iterator in e)for(e of e)yield*S(e);else"nodeName"in e?typeof e.nodeName=="function"?yield*O(e):yield e:yield String(e)},O=function*({nodeName:e,...t}){e.constructor.name=="GeneratorFunction"?yield T(e,t):yield*S(e(t))},T=(e,t)=>{const r={...e.attrs},s={...e.args};for(const n in t)n.startsWith("attr:")?r[n.slice(5)]=t[n]:n=="key"||n=="skip"||n=="memo"||n=="ref"||n.startsWith("set:")?r[n]=t[n]:s[n]=t[n];return{...r,nodeName:e.is??"div",[c]:e,[l]:s}},E=(e,t,r)=>typeof e=="string"?F(e,r):G(e,t,r),F=(e,t)=>{for(;t&&t.nodeType!=3;)t=t.nextSibling;return t?t.data!=e&&(t.data=e):t=document.createTextNode(e),t},G=({nodeName:e,children:t,key:r,skip:s,memo:n,ref:x,[c]:u,[l]:v,...w},C,i)=>{for(;i&&(i.localName!=e||i[y]!=null&&i[y]!=r||i[c]&&i[c]!=u);)i=i.nextSibling;return i??=document.createElementNS(w.xmlns??C.namespaceURI,e),r!=null&&(i[y]=r),(n==null||W(i[j],i[j]=n))&&(R(i[A]??B(i),i[A]=w,i),s||(u?I(u,v,i):f(t,i)),typeof x=="function"&&(i[g]=x)(i)),i},R=(e,t,r)=>{for(const s in{...e,...t})e[s]!==t[s]&&(s.startsWith("set:")?r[s.slice(4)]=t[s]:t[s]==null||t[s]===!1?r.removeAttribute(s):r.setAttribute(s,t[s]===!0?"":t[s]))},W=(e,t)=>Array.isArray(e)&&Array.isArray(t)?e.some((r,s)=>r!==t[s]):e!==t,B=e=>Array.from(e.attributes).reduce((t,r)=>(t[r.name]=r.value,t),{}),m=(e,t,r)=>{if(t.contains(document.activeElement)){const s=t.nextSibling;for(;r&&r!=t;){const n=r.nextSibling;e.insertBefore(r,s),r=n}}else e.insertBefore(t,r)},p=e=>{for(const t of e.children)p(t);typeof e.return=="function"&&e.return(),e[g]?.(null)},I=(e,t,r)=>{r[c]??=(M(r),e),Object.assign(r[l]??={},t),r[b]()},M=e=>{Object.assign(e,h),e[o.Context]=Object.create(o.current()?.[o.Context]??null)},h={[b](){const e=o.current();o.current(this);try{const{value:t,done:r}=(this[a]??=this[c].call(this,this[l])).next();f(t,this),this[g]?.(this),r&&this.return()}catch(t){this.throw(t)}finally{o.current(e)}},next(e){try{e?.call(this,this[l])}catch(t){return this.throw(t)}o.current()?.contains(this)||this[b]()},throw(e){for(let t=this;t;t=t.parentNode)if(t[a]?.throw)try{return f(t[a].throw(e).value,t)}catch(r){e=new Error(r?.message??r,{cause:e})}throw e},return(){try{this[a]?.return()}catch(e){this.throw(e)}finally{this[a]=null}}};exports.Fragment=N;exports.h=k;exports.render=f;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("./context.cjs"),y=Symbol.for("ajo.key"),j=Symbol.for("ajo.memo"),g=Symbol.for("ajo.ref"),A=Symbol.for("ajo.cache"),c=Symbol.for("ajo.generator"),o=Symbol.for("ajo.iterator"),b=Symbol.for("ajo.render"),l=Symbol.for("ajo.args"),p=Symbol.for("ajo.controller"),N=e=>e.children,k=(e,t,...r)=>((t??={}).nodeName=e,!("children"in t)&&r.length&&(t.children=r.length==1?r[0]:r),t),f=(e,t,r=t.firstChild,s=null)=>{for(e of S(e)){const n=E(e,t,r);r==null?m(t,n,s):n==r?r=n.nextSibling:n==r.nextSibling?(m(t,r,s),r=n.nextSibling):m(t,n,r)}for(;r!=s;){const n=r.nextSibling;r.nodeType==1&&C(r),t.removeChild(r),r=n}},S=function*(e){if(e==null)return;const t=typeof e;if(t!="boolean")if(t=="string")yield e;else if(t=="number"||t=="bigint")yield String(e);else if(Symbol.iterator in e)for(e of e)yield*S(e);else"nodeName"in e?typeof e.nodeName=="function"?yield*O(e):yield e:yield String(e)},O=function*({nodeName:e,...t}){e.constructor.name=="GeneratorFunction"?yield T(e,t):yield*S(e(t))},T=(e,t)=>{const r={...e.attrs},s={...e.args};for(const n in t)n.startsWith("attr:")?r[n.slice(5)]=t[n]:n=="key"||n=="skip"||n=="memo"||n=="ref"||n.startsWith("set:")?r[n]=t[n]:s[n]=t[n];return{...r,nodeName:e.is??"div",[c]:e,[l]:s}},E=(e,t,r)=>typeof e=="string"?F(e,r):G(e,t,r),F=(e,t)=>{for(;t&&t.nodeType!=3;)t=t.nextSibling;return t?t.data!=e&&(t.data=e):t=document.createTextNode(e),t},G=({nodeName:e,children:t,key:r,skip:s,memo:n,ref:x,[c]:u,[l]:h,...w},v,i)=>{for(;i&&(i.localName!=e||i[y]!=null&&i[y]!=r||i[c]&&i[c]!=u);)i=i.nextSibling;return i??=document.createElementNS(w.xmlns??v.namespaceURI,e),r!=null&&(i[y]=r),(n==null||W(i[j],i[j]=n))&&(R(i[A]??B(i),i[A]=w,i),s||(u?I(u,h,i):f(t,i)),typeof x=="function"&&(i[g]=x)(i)),i},R=(e,t,r)=>{for(const s in{...e,...t})e[s]!==t[s]&&(s.startsWith("set:")?r[s.slice(4)]=t[s]:t[s]==null||t[s]===!1?r.removeAttribute(s):r.setAttribute(s,t[s]===!0?"":t[s]))},W=(e,t)=>Array.isArray(e)&&Array.isArray(t)?e.some((r,s)=>r!==t[s]):e!==t,B=e=>Array.from(e.attributes).reduce((t,r)=>(t[r.name]=r.value,t),{}),m=(e,t,r)=>{if(t.contains(document.activeElement)){const s=t.nextSibling;for(;r&&r!=t;){const n=r.nextSibling;e.insertBefore(r,s),r=n}}else e.insertBefore(t,r)},C=e=>{for(const t of e.children)C(t);typeof e.return=="function"&&e.return(),e[g]?.(null)},I=(e,t,r)=>{r[c]??=(M(r),e),Object.assign(r[l]??={},t),r[b]()},M=e=>{Object.assign(e,q),e[a.Context]=Object.create(a.current()?.[a.Context]??null)},q={[b](){const e=a.current();a.current(this);try{this[o]||(this.signal=(this[p]=new AbortController).signal,this[o]=this[c].call(this,this[l]));const{value:t,done:r}=this[o].next();f(t,this),this[g]?.(this),r&&this.return()}catch(t){this.throw(t)}finally{a.current(e)}},next(e,t){try{typeof e=="function"&&(t=e.call(this,this[l]))}catch(r){return this.throw(r)}return a.current()?.contains(this)||this[b](),t},throw(e){for(let t=this;t;t=t.parentNode)if(t[o]?.throw)try{return f(t[o].throw(e).value,t)}catch(r){e=new Error(r?.message??r,{cause:e})}throw e},return(){try{this[o]?.return()}catch(e){this.throw(e)}finally{this[o]=null,this[p]?.abort()}}};exports.Fragment=N;exports.h=k;exports.render=f;
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
- import { Context as j, current as c } from "./context.js";
2
- const u = /* @__PURE__ */ Symbol.for("ajo.key"), p = /* @__PURE__ */ Symbol.for("ajo.memo"), g = /* @__PURE__ */ Symbol.for("ajo.ref"), A = /* @__PURE__ */ Symbol.for("ajo.cache"), o = /* @__PURE__ */ Symbol.for("ajo.generator"), a = /* @__PURE__ */ Symbol.for("ajo.iterator"), m = /* @__PURE__ */ Symbol.for("ajo.render"), l = /* @__PURE__ */ Symbol.for("ajo.args"), U = (e) => e.children, h = (e, t, ...r) => ((t ??= {}).nodeName = e, !("children" in t) && r.length && (t.children = r.length == 1 ? r[0] : r), t), b = (e, t, r = t.firstChild, s = null) => {
1
+ import { Context as j, current as l } from "./context.js";
2
+ const u = /* @__PURE__ */ Symbol.for("ajo.key"), p = /* @__PURE__ */ Symbol.for("ajo.memo"), g = /* @__PURE__ */ Symbol.for("ajo.ref"), A = /* @__PURE__ */ Symbol.for("ajo.cache"), a = /* @__PURE__ */ Symbol.for("ajo.generator"), o = /* @__PURE__ */ Symbol.for("ajo.iterator"), m = /* @__PURE__ */ Symbol.for("ajo.render"), c = /* @__PURE__ */ Symbol.for("ajo.args"), C = /* @__PURE__ */ Symbol.for("ajo.controller"), U = (e) => e.children, q = (e, t, ...r) => ((t ??= {}).nodeName = e, !("children" in t) && r.length && (t.children = r.length == 1 ? r[0] : r), t), b = (e, t, r = t.firstChild, s = null) => {
3
3
  for (e of S(e)) {
4
4
  const n = G(e, t, r);
5
5
  r == null ? y(t, n, s) : n == r ? r = n.nextSibling : n == r.nextSibling ? (y(t, r, s), r = n.nextSibling) : y(t, n, r);
6
6
  }
7
7
  for (; r != s; ) {
8
8
  const n = r.nextSibling;
9
- r.nodeType == 1 && N(r), t.removeChild(r), r = n;
9
+ r.nodeType == 1 && h(r), t.removeChild(r), r = n;
10
10
  }
11
11
  }, S = function* (e) {
12
12
  if (e == null) return;
@@ -15,20 +15,20 @@ const u = /* @__PURE__ */ Symbol.for("ajo.key"), p = /* @__PURE__ */ Symbol.for(
15
15
  if (t == "string") yield e;
16
16
  else if (t == "number" || t == "bigint") yield String(e);
17
17
  else if (Symbol.iterator in e) for (e of e) yield* S(e);
18
- else "nodeName" in e ? typeof e.nodeName == "function" ? yield* C(e) : yield e : yield String(e);
19
- }, C = function* ({ nodeName: e, ...t }) {
18
+ else "nodeName" in e ? typeof e.nodeName == "function" ? yield* v(e) : yield e : yield String(e);
19
+ }, v = function* ({ nodeName: e, ...t }) {
20
20
  e.constructor.name == "GeneratorFunction" ? yield E(e, t) : yield* S(e(t));
21
21
  }, E = (e, t) => {
22
22
  const r = { ...e.attrs }, s = { ...e.args };
23
23
  for (const n in t)
24
24
  n.startsWith("attr:") ? r[n.slice(5)] = t[n] : n == "key" || n == "skip" || n == "memo" || n == "ref" || n.startsWith("set:") ? r[n] = t[n] : s[n] = t[n];
25
- return { ...r, nodeName: e.is ?? "div", [o]: e, [l]: s };
25
+ return { ...r, nodeName: e.is ?? "div", [a]: e, [c]: s };
26
26
  }, G = (e, t, r) => typeof e == "string" ? O(e, r) : R(e, t, r), O = (e, t) => {
27
27
  for (; t && t.nodeType != 3; ) t = t.nextSibling;
28
28
  return t ? t.data != e && (t.data = e) : t = document.createTextNode(e), t;
29
- }, R = ({ nodeName: e, children: t, key: r, skip: s, memo: n, ref: x, [o]: f, [l]: k, ...w }, v, i) => {
30
- for (; i && (i.localName != e || i[u] != null && i[u] != r || i[o] && i[o] != f); ) i = i.nextSibling;
31
- return i ??= document.createElementNS(w.xmlns ?? v.namespaceURI, e), r != null && (i[u] = r), (n == null || W(i[p], i[p] = n)) && (T(i[A] ?? B(i), i[A] = w, i), s || (f ? F(f, k, i) : b(t, i)), typeof x == "function" && (i[g] = x)(i)), i;
29
+ }, R = ({ nodeName: e, children: t, key: r, skip: s, memo: n, ref: x, [a]: f, [c]: N, ...w }, k, i) => {
30
+ for (; i && (i.localName != e || i[u] != null && i[u] != r || i[a] && i[a] != f); ) i = i.nextSibling;
31
+ return i ??= document.createElementNS(w.xmlns ?? k.namespaceURI, e), r != null && (i[u] = r), (n == null || W(i[p], i[p] = n)) && (T(i[A] ?? B(i), i[A] = w, i), s || (f ? F(f, N, i) : b(t, i)), typeof x == "function" && (i[g] = x)(i)), i;
32
32
  }, T = (e, t, r) => {
33
33
  for (const s in { ...e, ...t })
34
34
  e[s] !== t[s] && (s.startsWith("set:") ? r[s.slice(4)] = t[s] : t[s] == null || t[s] === !1 ? r.removeAttribute(s) : r.setAttribute(s, t[s] === !0 ? "" : t[s]));
@@ -40,37 +40,38 @@ const u = /* @__PURE__ */ Symbol.for("ajo.key"), p = /* @__PURE__ */ Symbol.for(
40
40
  e.insertBefore(r, s), r = n;
41
41
  }
42
42
  } else e.insertBefore(t, r);
43
- }, N = (e) => {
44
- for (const t of e.children) N(t);
43
+ }, h = (e) => {
44
+ for (const t of e.children) h(t);
45
45
  typeof e.return == "function" && e.return(), e[g]?.(null);
46
46
  }, F = (e, t, r) => {
47
- r[o] ??= (I(r), e), Object.assign(r[l] ??= {}, t), r[m]();
47
+ r[a] ??= (I(r), e), Object.assign(r[c] ??= {}, t), r[m]();
48
48
  }, I = (e) => {
49
- Object.assign(e, K), e[j] = Object.create(c()?.[j] ?? null);
49
+ Object.assign(e, K), e[j] = Object.create(l()?.[j] ?? null);
50
50
  }, K = {
51
51
  [m]() {
52
- const e = c();
53
- c(this);
52
+ const e = l();
53
+ l(this);
54
54
  try {
55
- const { value: t, done: r } = (this[a] ??= this[o].call(this, this[l])).next();
55
+ this[o] || (this.signal = (this[C] = new AbortController()).signal, this[o] = this[a].call(this, this[c]));
56
+ const { value: t, done: r } = this[o].next();
56
57
  b(t, this), this[g]?.(this), r && this.return();
57
58
  } catch (t) {
58
59
  this.throw(t);
59
60
  } finally {
60
- c(e);
61
+ l(e);
61
62
  }
62
63
  },
63
- next(e) {
64
+ next(e, t) {
64
65
  try {
65
- e?.call(this, this[l]);
66
- } catch (t) {
67
- return this.throw(t);
66
+ typeof e == "function" && (t = e.call(this, this[c]));
67
+ } catch (r) {
68
+ return this.throw(r);
68
69
  }
69
- c()?.contains(this) || this[m]();
70
+ return l()?.contains(this) || this[m](), t;
70
71
  },
71
72
  throw(e) {
72
- for (let t = this; t; t = t.parentNode) if (t[a]?.throw) try {
73
- return b(t[a].throw(e).value, t);
73
+ for (let t = this; t; t = t.parentNode) if (t[o]?.throw) try {
74
+ return b(t[o].throw(e).value, t);
74
75
  } catch (r) {
75
76
  e = new Error(r?.message ?? r, { cause: e });
76
77
  }
@@ -78,16 +79,16 @@ const u = /* @__PURE__ */ Symbol.for("ajo.key"), p = /* @__PURE__ */ Symbol.for(
78
79
  },
79
80
  return() {
80
81
  try {
81
- this[a]?.return();
82
+ this[o]?.return();
82
83
  } catch (e) {
83
84
  this.throw(e);
84
85
  } finally {
85
- this[a] = null;
86
+ this[o] = null, this[C]?.abort();
86
87
  }
87
88
  }
88
89
  };
89
90
  export {
90
91
  U as Fragment,
91
- h,
92
+ q as h,
92
93
  b as render
93
94
  };
package/html.js ADDED
@@ -0,0 +1,142 @@
1
+ import { Context, current } from 'ajo/context'
2
+
3
+ const Void = new Set(['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'])
4
+
5
+ const Args = Symbol.for('ajo.args')
6
+
7
+ const escape = s => s.replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`)
8
+
9
+ const noop = () => { }
10
+
11
+ export const render = h => [...html(h)].join('')
12
+
13
+ export const html = function* (h) {
14
+
15
+ for (h of walk(h)) {
16
+
17
+ if (typeof h == 'string') yield escape(h)
18
+
19
+ else yield* element(h)
20
+ }
21
+ }
22
+
23
+ const element = function* ({ nodeName, children, ...h }) {
24
+
25
+ let attrs = ''
26
+
27
+ for (const key in h) {
28
+
29
+ if (key.startsWith('set:') || h[key] == null || h[key] === false) continue
30
+
31
+ if (h[key] === true) attrs += ` ${key}`
32
+
33
+ else attrs += ` ${key}="${escape(String(h[key]))}"`
34
+ }
35
+
36
+ if (Void.has(nodeName)) yield `<${nodeName}${attrs}>`
37
+
38
+ else {
39
+
40
+ yield `<${nodeName}${attrs}>`
41
+
42
+ if (children != null) yield* html(children)
43
+
44
+ yield `</${nodeName}>`
45
+ }
46
+ }
47
+
48
+ const walk = function* (h) {
49
+
50
+ if (h == null) return
51
+
52
+ const type = typeof h
53
+
54
+ if (type == 'boolean') return
55
+
56
+ if (type == 'string') yield h
57
+
58
+ else if (type == 'number' || type == 'bigint') yield String(h)
59
+
60
+ else if (Symbol.iterator in h) for (h of h) yield* walk(h)
61
+
62
+ else if ('nodeName' in h) typeof h.nodeName == 'function' ? yield* run(h) : yield vdom(h)
63
+
64
+ else yield String(h)
65
+ }
66
+
67
+ const run = function* ({ nodeName, fallback = nodeName.fallback, ...h }) {
68
+
69
+ if (nodeName.constructor.name == 'GeneratorFunction') yield runGenerator(nodeName, h)
70
+
71
+ else yield* walk(nodeName(h))
72
+ }
73
+
74
+ const runGenerator = (fn, h) => {
75
+
76
+ const attrs = { ...fn.attrs }, args = { ...fn.args }
77
+
78
+ for (const key in h) {
79
+
80
+ if (key.startsWith('attr:')) attrs[key.slice(5)] = h[key]
81
+
82
+ else if (key == 'key' || key == 'skip' || key == 'memo' || key == 'ref' || key.startsWith('set:')) attrs[key] = h[key]
83
+
84
+ else args[key] = h[key]
85
+ }
86
+
87
+ const controller = new AbortController()
88
+
89
+ const instance = {
90
+
91
+ [Context]: Object.create(current()?.[Context] ?? null),
92
+
93
+ [Args]: args,
94
+
95
+ signal: controller.signal,
96
+
97
+ next: noop,
98
+
99
+ return: noop,
100
+
101
+ throw: value => { throw value }
102
+ }
103
+
104
+ const iterator = fn.call(instance, args)
105
+
106
+ const parent = current()
107
+
108
+ current(instance)
109
+
110
+ const result = children => ({ ...attrs, nodeName: fn.is ?? 'div', ...vdom({ children }) })
111
+
112
+ try {
113
+
114
+ return result(iterator.next().value)
115
+
116
+ } catch (error) {
117
+
118
+ return result(iterator.throw(error).value)
119
+
120
+ } finally {
121
+
122
+ iterator.return()
123
+
124
+ controller.abort()
125
+
126
+ current(parent)
127
+ }
128
+ }
129
+
130
+ const vdom = ({ key, skip, memo, ref, ...h }) => {
131
+
132
+ if ('children' in h) {
133
+
134
+ const children = [...walk(h.children)]
135
+
136
+ if (children.length) h.children = children.length == 1 ? children[0] : children
137
+
138
+ else delete h.children
139
+ }
140
+
141
+ return h
142
+ }
package/index.js ADDED
@@ -0,0 +1,286 @@
1
+ import { Context, current } from 'ajo/context'
2
+
3
+ const Key = Symbol.for('ajo.key')
4
+ const Memo = Symbol.for('ajo.memo')
5
+ const Ref = Symbol.for('ajo.ref')
6
+ const Cache = Symbol.for('ajo.cache')
7
+ const Generator = Symbol.for('ajo.generator')
8
+ const Iterator = Symbol.for('ajo.iterator')
9
+ const Render = Symbol.for('ajo.render')
10
+ const Args = Symbol.for('ajo.args')
11
+ const Controller = Symbol.for('ajo.controller')
12
+
13
+ export const Fragment = props => props.children
14
+
15
+ export const h = (type, props, ...children) => {
16
+
17
+ (props ??= {}).nodeName = type
18
+
19
+ if (!('children' in props) && children.length) props.children = children.length == 1 ? children[0] : children
20
+
21
+ return props
22
+ }
23
+
24
+ export const render = (h, el, child = el.firstChild, ref = null) => {
25
+
26
+ for (h of walk(h)) {
27
+
28
+ const node = reconcile(h, el, child)
29
+
30
+ if (child == null) {
31
+
32
+ before(el, node, ref)
33
+
34
+ } else if (node == child) {
35
+
36
+ child = node.nextSibling
37
+
38
+ } else if (node == child.nextSibling) {
39
+
40
+ before(el, child, ref)
41
+
42
+ child = node.nextSibling
43
+
44
+ } else {
45
+
46
+ before(el, node, child)
47
+ }
48
+ }
49
+
50
+ while (child != ref) {
51
+
52
+ const node = child.nextSibling
53
+
54
+ if (child.nodeType == 1) unref(child)
55
+
56
+ el.removeChild(child)
57
+
58
+ child = node
59
+ }
60
+ }
61
+
62
+ const walk = function* (h) {
63
+
64
+ if (h == null) return
65
+
66
+ const type = typeof h
67
+
68
+ if (type == 'boolean') return
69
+
70
+ if (type == 'string') yield h
71
+
72
+ else if (type == 'number' || type == 'bigint') yield String(h)
73
+
74
+ else if (Symbol.iterator in h) for (h of h) yield* walk(h)
75
+
76
+ else if ('nodeName' in h) typeof h.nodeName == 'function' ? yield* run(h) : yield h
77
+
78
+ else yield String(h)
79
+ }
80
+
81
+ const run = function* ({ nodeName, ...h }) {
82
+
83
+ if (nodeName.constructor.name == 'GeneratorFunction') yield runGenerator(nodeName, h)
84
+
85
+ else yield* walk(nodeName(h))
86
+ }
87
+
88
+ const runGenerator = (fn, h) => {
89
+
90
+ const attrs = { ...fn.attrs }, args = { ...fn.args }
91
+
92
+ for (const key in h) {
93
+
94
+ if (key.startsWith('attr:')) attrs[key.slice(5)] = h[key]
95
+
96
+ else if (key == 'key' || key == 'skip' || key == 'memo' || key == 'ref' || key.startsWith('set:')) attrs[key] = h[key]
97
+
98
+ else args[key] = h[key]
99
+ }
100
+
101
+ return { ...attrs, nodeName: fn.is ?? 'div', [Generator]: fn, [Args]: args }
102
+ }
103
+
104
+ const reconcile = (h, el, node) => typeof h == 'string' ? text(h, node) : element(h, el, node)
105
+
106
+ const text = (h, node) => {
107
+
108
+ while (node && node.nodeType != 3) node = node.nextSibling
109
+
110
+ node ? node.data != h && (node.data = h) : node = document.createTextNode(h)
111
+
112
+ return node
113
+ }
114
+
115
+ const element = ({ nodeName, children, key, skip, memo, ref, [Generator]: gen, [Args]: args, ...h }, el, node) => {
116
+
117
+ while (node && (
118
+
119
+ (node.localName != nodeName) ||
120
+
121
+ (node[Key] != null && node[Key] != key) ||
122
+
123
+ (node[Generator] && node[Generator] != gen)
124
+
125
+ )) node = node.nextSibling
126
+
127
+ node ??= document.createElementNS(h.xmlns ?? el.namespaceURI, nodeName)
128
+
129
+ if (key != null) node[Key] = key
130
+
131
+ if (memo == null || some(node[Memo], node[Memo] = memo)) {
132
+
133
+ attrs(node[Cache] ?? extract(node), node[Cache] = h, node)
134
+
135
+ if (!skip) gen ? next(gen, args, node) : render(children, node)
136
+
137
+ if (typeof ref == 'function') (node[Ref] = ref)(node)
138
+ }
139
+
140
+ return node
141
+ }
142
+
143
+ const attrs = (cache, h, node) => {
144
+
145
+ for (const key in { ...cache, ...h }) {
146
+
147
+ if (cache[key] === h[key]) continue
148
+
149
+ if (key.startsWith('set:')) node[key.slice(4)] = h[key]
150
+
151
+ else if (h[key] == null || h[key] === false) node.removeAttribute(key)
152
+
153
+ else node.setAttribute(key, h[key] === true ? '' : h[key])
154
+ }
155
+ }
156
+
157
+ const some = (a, b) => Array.isArray(a) && Array.isArray(b) ? a.some((v, i) => v !== b[i]) : a !== b
158
+
159
+ const extract = el => Array.from(el.attributes).reduce((out, attr) => (out[attr.name] = attr.value, out), {})
160
+
161
+ const before = (el, node, child) => {
162
+
163
+ if (node.contains(document.activeElement)) {
164
+
165
+ const ref = node.nextSibling
166
+
167
+ while (child && child != node) {
168
+
169
+ const next = child.nextSibling
170
+
171
+ el.insertBefore(child, ref)
172
+
173
+ child = next
174
+ }
175
+
176
+ } else el.insertBefore(node, child)
177
+ }
178
+
179
+ const unref = node => {
180
+
181
+ for (const child of node.children) unref(child)
182
+
183
+ if (typeof node.return == 'function') node.return()
184
+
185
+ node[Ref]?.(null)
186
+ }
187
+
188
+ const next = (fn, args, el) => {
189
+
190
+ el[Generator] ??= (attach(el), fn)
191
+
192
+ Object.assign(el[Args] ??= {}, args)
193
+
194
+ el[Render]()
195
+ }
196
+
197
+ const attach = el => {
198
+
199
+ Object.assign(el, methods)
200
+
201
+ el[Context] = Object.create(current()?.[Context] ?? null)
202
+ }
203
+
204
+ const methods = {
205
+
206
+ [Render]() {
207
+
208
+ const parent = current()
209
+
210
+ current(this)
211
+
212
+ try {
213
+
214
+ if (!this[Iterator]) {
215
+
216
+ this.signal = (this[Controller] = new AbortController()).signal
217
+
218
+ this[Iterator] = this[Generator].call(this, this[Args])
219
+ }
220
+
221
+ const { value, done } = this[Iterator].next()
222
+
223
+ render(value, this)
224
+
225
+ this[Ref]?.(this)
226
+
227
+ if (done) this.return()
228
+
229
+ } catch (e) {
230
+
231
+ this.throw(e)
232
+
233
+ } finally {
234
+
235
+ current(parent)
236
+ }
237
+ },
238
+
239
+ next(fn, result) {
240
+
241
+ try {
242
+
243
+ if (typeof fn == 'function') result = fn.call(this, this[Args])
244
+
245
+ } catch (e) {
246
+
247
+ return this.throw(e)
248
+ }
249
+
250
+ if (!current()?.contains(this)) this[Render]()
251
+
252
+ return result
253
+ },
254
+
255
+ throw(value) {
256
+
257
+ for (let el = this; el; el = el.parentNode) if (el[Iterator]?.throw) try {
258
+
259
+ return render(el[Iterator].throw(value).value, el)
260
+
261
+ } catch (e) {
262
+
263
+ value = new Error(e?.message ?? e, { cause: value })
264
+ }
265
+
266
+ throw value
267
+ },
268
+
269
+ return() {
270
+
271
+ try {
272
+
273
+ this[Iterator]?.return()
274
+
275
+ } catch (e) {
276
+
277
+ this.throw(e)
278
+
279
+ } finally {
280
+
281
+ this[Iterator] = null
282
+
283
+ this[Controller]?.abort()
284
+ }
285
+ }
286
+ }
package/license CHANGED
@@ -1,6 +1,6 @@
1
1
  ISC License
2
2
 
3
- Copyright (c) 2022-2025, Cristian Falcone
3
+ Copyright (c) 2022-2026, Cristian Falcone
4
4
 
5
5
  Permission to use, copy, modify, and/or distribute this software for any
6
6
  purpose with or without fee is hereby granted, provided that the above
package/package.json CHANGED
@@ -1,55 +1,59 @@
1
1
  {
2
- "name": "ajo",
3
- "version": "0.1.30",
4
- "description": "ajo is a JavaScript view library for building user interfaces",
5
- "type": "module",
6
- "types": "./types.ts",
7
- "module": "./dist/index.js",
8
- "main": "./dist/index.cjs",
9
- "exports": {
10
- ".": {
11
- "types": "./types.ts",
12
- "import": "./dist/index.js",
13
- "require": "./dist/index.cjs"
14
- },
15
- "./context": {
16
- "types": "./types.ts",
17
- "import": "./dist/context.js",
18
- "require": "./dist/context.cjs"
19
- },
20
- "./html": {
21
- "types": "./types.ts",
22
- "import": "./dist/html.js",
23
- "require": "./dist/html.cjs"
24
- }
25
- },
26
- "files": [
27
- "dist",
28
- "types.ts"
29
- ],
30
- "scripts": {
31
- "test": "vitest --run",
32
- "build": "vite build",
33
- "bump": "pnpm version patch && git push && git push --tags",
34
- "release": "pnpm test && pnpm build && pnpm bump && pnpm publish"
35
- },
36
- "devDependencies": {
37
- "@types/node": "25.2.3",
38
- "happy-dom": "20.6.1",
39
- "vite": "7.3.1",
40
- "vite-tsconfig-paths": "6.1.1",
41
- "vitest": "4.0.18"
42
- },
43
- "keywords": [
44
- "ui",
45
- "frontend",
46
- "web",
47
- "dom",
48
- "jsx"
49
- ],
50
- "repository": "cristianfalcone/ajo",
51
- "author": "Cristian Falcone",
52
- "license": "ISC",
53
- "bugs": "https://github.com/cristianfalcone/ajo/issues",
54
- "homepage": "https://github.com/cristianfalcone/ajo#readme"
55
- }
2
+ "name": "ajo",
3
+ "version": "0.1.31",
4
+ "description": "ajo is a JavaScript view library for building user interfaces",
5
+ "type": "module",
6
+ "types": "./types.ts",
7
+ "module": "./dist/index.js",
8
+ "main": "./dist/index.cjs",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./types.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./context": {
16
+ "types": "./types.ts",
17
+ "import": "./dist/context.js",
18
+ "require": "./dist/context.cjs"
19
+ },
20
+ "./html": {
21
+ "types": "./types.ts",
22
+ "import": "./dist/html.js",
23
+ "require": "./dist/html.cjs"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "context.js",
29
+ "html.js",
30
+ "index.js",
31
+ "LLMs.md",
32
+ "types.ts"
33
+ ],
34
+ "devDependencies": {
35
+ "@types/node": "25.2.3",
36
+ "happy-dom": "20.6.1",
37
+ "vite": "7.3.1",
38
+ "vite-tsconfig-paths": "6.1.1",
39
+ "vitest": "4.0.18"
40
+ },
41
+ "keywords": [
42
+ "ui",
43
+ "frontend",
44
+ "web",
45
+ "dom",
46
+ "jsx"
47
+ ],
48
+ "repository": "cristianfalcone/ajo",
49
+ "author": "Cristian Falcone",
50
+ "license": "ISC",
51
+ "bugs": "https://github.com/cristianfalcone/ajo/issues",
52
+ "homepage": "https://github.com/cristianfalcone/ajo#readme",
53
+ "scripts": {
54
+ "test": "vitest --run",
55
+ "build": "vite build",
56
+ "bump": "pnpm version patch && git push && git push --tags",
57
+ "release": "pnpm test && pnpm build && pnpm bump && pnpm publish"
58
+ }
59
+ }
package/readme.md CHANGED
@@ -125,7 +125,7 @@ function* TodoList() {
125
125
 
126
126
  ### Re-rendering with `this.next()`
127
127
 
128
- Call `this.next()` to trigger a re-render. The optional callback receives current props, useful when props may have changed:
128
+ Call `this.next()` to trigger a re-render. The optional callback receives current props and its return value is passed through:
129
129
 
130
130
  ```javascript
131
131
  function* Stepper(args) {
@@ -158,7 +158,22 @@ function* Good(args) {
158
158
 
159
159
  ### Lifecycle and Cleanup
160
160
 
161
- Use `try...finally` for cleanup when the component unmounts:
161
+ Every stateful component has a `this.signal` (AbortSignal) that aborts when the component unmounts. Use it with any API that accepts a signal:
162
+
163
+ ```javascript
164
+ function* MouseTracker() {
165
+
166
+ let pos = { x: 0, y: 0 }
167
+
168
+ document.addEventListener('mousemove', e => this.next(() => {
169
+ pos = { x: e.clientX, y: e.clientY }
170
+ }), { signal: this.signal }) // auto-removed on unmount
171
+
172
+ while (true) yield <p>{pos.x}, {pos.y}</p>
173
+ }
174
+ ```
175
+
176
+ For APIs that don't accept a signal, use `try...finally`:
162
177
 
163
178
  ```javascript
164
179
  function* Clock() {
@@ -329,7 +344,7 @@ function* UserProfile(args) {
329
344
 
330
345
  let data = null, error = null, loading = true
331
346
 
332
- fetch(`/api/users/${args.id}`)
347
+ fetch(`/api/users/${args.id}`, { signal: this.signal })
333
348
  .then(r => r.json())
334
349
  .then(d => this.next(() => { data = d; loading = false }))
335
350
  .catch(e => this.next(() => { error = e; loading = false }))
@@ -401,9 +416,10 @@ let ref: ThisParameterType<typeof Counter> | null = null
401
416
  | `render(children)` | Render to HTML string |
402
417
 
403
418
  ### Stateful `this`
404
- | Method | Description |
405
- |--------|-------------|
406
- | `this.next(fn?)` | Re-render. Callback receives current args. |
419
+ | Property | Description |
420
+ |----------|-------------|
421
+ | `this.signal` | AbortSignal that aborts on unmount. Pass to `fetch()`, `addEventListener()`, etc. |
422
+ | `this.next(fn?)` | Re-render. Callback receives current args. Returns callback's result. |
407
423
  | `this.throw(error)` | Throw to parent boundary |
408
424
  | `this.return()` | Terminate generator |
409
425
 
package/types.ts CHANGED
@@ -53,7 +53,8 @@ declare module 'ajo' {
53
53
  TArguments
54
54
 
55
55
  type StatefulElement<TArguments, TTag> = ElementType<TTag> & {
56
- next: (fn?: (this: StatefulElement<TArguments, TTag>, args: StatefulArgs<TArguments, TTag>) => void) => void,
56
+ signal: AbortSignal,
57
+ next: <R>(fn?: (this: StatefulElement<TArguments, TTag>, args: StatefulArgs<TArguments, TTag>) => R) => R,
57
58
  throw: (value?: unknown) => void,
58
59
  return: () => void,
59
60
  }