@vanijs/vani 0.2.0 → 0.3.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/DOCS.md +221 -35
- package/README.md +51 -3
- package/dist/lib/index.d.mts +99 -833
- package/dist/lib/index.mjs +1 -1
- package/dist/lib/jsx-dev-runtime.d.mts +2 -0
- package/dist/lib/jsx-dev-runtime.mjs +1 -0
- package/dist/lib/jsx-runtime.d.mts +27 -0
- package/dist/lib/jsx-runtime.mjs +1 -0
- package/dist/lib/runtime-BIk4OH1t.mjs +1 -0
- package/dist/lib/runtime-Dp-nlil7.d.mts +166 -0
- package/package.json +60 -19
package/DOCS.md
CHANGED
|
@@ -47,6 +47,119 @@ renderToDOM([Counter()], appRoot)
|
|
|
47
47
|
|
|
48
48
|
---
|
|
49
49
|
|
|
50
|
+
## JSX mode examples
|
|
51
|
+
|
|
52
|
+
Vani is JS-first and transpiler-free, with an optional JSX adapter. JSX mode requires
|
|
53
|
+
`jsxImportSource` to be set to `@vanijs/vani` and a `.tsx` file.
|
|
54
|
+
|
|
55
|
+
### 1) JSX counter button
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { component, renderToDOM, type Handle } from '@vanijs/vani'
|
|
59
|
+
|
|
60
|
+
const Counter = component((_, handle: Handle) => {
|
|
61
|
+
let count = 0
|
|
62
|
+
return () => (
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
onclick={() => {
|
|
66
|
+
count += 1
|
|
67
|
+
handle.update()
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
Count: {count}
|
|
71
|
+
</button>
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
renderToDOM([Counter()], document.getElementById('app')!)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 2) JSX component inside JS-first components
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { component } from '@vanijs/vani'
|
|
82
|
+
import * as h from '@vanijs/vani/html'
|
|
83
|
+
|
|
84
|
+
const Badge = component<{ label: string }>((props) => {
|
|
85
|
+
return () => <span>{props.label}</span>
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const Panel = component(() => {
|
|
89
|
+
return () =>
|
|
90
|
+
h.div(
|
|
91
|
+
'Mixed render:',
|
|
92
|
+
Badge({ label: 'JSX component' }),
|
|
93
|
+
h.span('inside a JS-first component.'),
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Incremental adoption (mounting points)
|
|
101
|
+
|
|
102
|
+
Vani is intentionally small and lightweight, so you don't need to replace your full stack. You can
|
|
103
|
+
adopt it gradually by mounting Vani components inside existing apps (React, Vue, server-rendered
|
|
104
|
+
pages, etc.) using plain DOM elements as mounting points.
|
|
105
|
+
|
|
106
|
+
Benefits:
|
|
107
|
+
|
|
108
|
+
- Gradual migration: move one widget or screen at a time without a rewrite.
|
|
109
|
+
- Minimal surface area: no global runtime or framework lock-in.
|
|
110
|
+
- Clear ownership: a Vani component owns only its subtree between anchors.
|
|
111
|
+
- Easy rollback: remove a mount point and the rest of the app keeps working.
|
|
112
|
+
|
|
113
|
+
### Example: mount a Vani widget inside React
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
import { useEffect, useRef } from 'react'
|
|
117
|
+
import { component, div, button, renderToDOM, type Handle } from '@vanijs/vani'
|
|
118
|
+
|
|
119
|
+
// Vani component (local state + explicit updates)
|
|
120
|
+
const VaniCounter = component((_, handle) => {
|
|
121
|
+
let count = 0
|
|
122
|
+
return () =>
|
|
123
|
+
div(
|
|
124
|
+
`Count: ${count}`,
|
|
125
|
+
button(
|
|
126
|
+
{
|
|
127
|
+
onclick: () => {
|
|
128
|
+
count += 1
|
|
129
|
+
handle.update()
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
'Increment',
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// React component that hosts the Vani widget
|
|
138
|
+
export function MyReactComponent() {
|
|
139
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
140
|
+
const vaniHandlesRef = useRef<Handle[] | null>(null)
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (!containerRef.current) return
|
|
144
|
+
|
|
145
|
+
// Mount Vani into a React-managed DOM element (the mounting point)
|
|
146
|
+
vaniHandlesRef.current = renderToDOM([VaniCounter()], containerRef.current)
|
|
147
|
+
|
|
148
|
+
// Cleanup when React unmounts
|
|
149
|
+
return () => {
|
|
150
|
+
for (const handle of vaniHandlesRef.current ?? []) {
|
|
151
|
+
handle.dispose()
|
|
152
|
+
}
|
|
153
|
+
vaniHandlesRef.current = null
|
|
154
|
+
}
|
|
155
|
+
}, [])
|
|
156
|
+
|
|
157
|
+
return <div ref={containerRef} />
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
50
163
|
## Core Concepts
|
|
51
164
|
|
|
52
165
|
### 1) Components are functions
|
|
@@ -516,19 +629,27 @@ export const emit = (event: string, payload?: unknown) => {
|
|
|
516
629
|
|
|
517
630
|
Vani scales best when you keep update paths explicit and module boundaries clear. The core idea is
|
|
518
631
|
to let feature modules own their local state and expose small, explicit APIs for coordination,
|
|
519
|
-
instead of reaching into each other
|
|
632
|
+
instead of reaching into each other's state or relying on global reactive graphs. At scale, treat
|
|
633
|
+
updates like messages: state lives in a module, views read from the module, and invalidation is
|
|
634
|
+
triggered by module commands.
|
|
520
635
|
|
|
521
636
|
### Suggested architecture
|
|
522
637
|
|
|
523
|
-
1. Feature modules
|
|
638
|
+
1. Feature modules (state + commands)
|
|
524
639
|
|
|
525
640
|
Each module exposes:
|
|
526
641
|
|
|
527
642
|
- a small state container
|
|
528
643
|
- read accessors (snapshot getters)
|
|
529
|
-
- explicit
|
|
644
|
+
- explicit commands (mutations) that notify listeners
|
|
645
|
+
- a `subscribe(listener)` for views to bind invalidation
|
|
646
|
+
|
|
647
|
+
2. View adapters (bind handles)
|
|
648
|
+
|
|
649
|
+
Views subscribe once via `handle.effect()` and call `handle.update()` when their module notifies.
|
|
650
|
+
This keeps invalidation scoped to the subtree that owns the handle.
|
|
530
651
|
|
|
531
|
-
|
|
652
|
+
3. Coordinator (optional)
|
|
532
653
|
|
|
533
654
|
For cross-module workflows, add a thin coordinator that:
|
|
534
655
|
|
|
@@ -536,74 +657,139 @@ For cross-module workflows, add a thin coordinator that:
|
|
|
536
657
|
- calls public APIs of each module
|
|
537
658
|
- never accesses private state directly
|
|
538
659
|
|
|
539
|
-
|
|
660
|
+
4. Stable, explicit contracts
|
|
540
661
|
|
|
541
662
|
Use interfaces, simple message payloads, or callbacks to avoid implicit coupling. If one feature
|
|
542
|
-
needs another to update, it calls that module
|
|
543
|
-
|
|
663
|
+
needs another to update, it calls that module's exported command (or `invalidate()` helper) rather
|
|
664
|
+
than mutating shared data.
|
|
544
665
|
|
|
545
666
|
### Example: Feature module with explicit invalidation
|
|
546
667
|
|
|
547
668
|
```ts
|
|
548
|
-
import { component, div, type Handle } from '@vanijs/vani'
|
|
669
|
+
import { component, div, type Handle, type Component } from '@vanijs/vani'
|
|
549
670
|
|
|
550
671
|
type User = { id: string; name: string }
|
|
551
672
|
|
|
552
|
-
export type
|
|
673
|
+
export type UsersFeatureApi = {
|
|
553
674
|
getUsers: () => User[]
|
|
554
|
-
|
|
675
|
+
setUsers: (next: User[]) => void
|
|
676
|
+
refreshUsers: () => Promise<void>
|
|
677
|
+
subscribe: (listener: () => void) => () => void
|
|
555
678
|
}
|
|
556
679
|
|
|
557
|
-
export const
|
|
680
|
+
export const createUsersFeature = (): { api: UsersFeatureApi; View: Component } => {
|
|
558
681
|
let users: User[] = []
|
|
682
|
+
const listeners = new Set<() => void>()
|
|
559
683
|
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
const setUsers = (next: User[]) => {
|
|
563
|
-
users = next
|
|
564
|
-
handle.update()
|
|
684
|
+
const notify = () => {
|
|
685
|
+
for (const listener of listeners) listener()
|
|
565
686
|
}
|
|
566
687
|
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
688
|
+
const api: UsersFeatureApi = {
|
|
689
|
+
getUsers: () => users,
|
|
690
|
+
setUsers: (next) => {
|
|
691
|
+
users = next
|
|
692
|
+
notify()
|
|
693
|
+
},
|
|
694
|
+
refreshUsers: async () => {
|
|
695
|
+
const response = await fetch('/api/users')
|
|
696
|
+
const data = (await response.json()) as User[]
|
|
697
|
+
api.setUsers(data)
|
|
698
|
+
},
|
|
699
|
+
subscribe: (listener) => {
|
|
700
|
+
listeners.add(listener)
|
|
701
|
+
return () => listeners.delete(listener)
|
|
702
|
+
},
|
|
571
703
|
}
|
|
572
704
|
|
|
573
|
-
|
|
705
|
+
const View = component((_, handle: Handle) => {
|
|
706
|
+
handle.effect(() => api.subscribe(() => handle.update()))
|
|
707
|
+
return () => div(api.getUsers().map((user) => div(user.name)))
|
|
708
|
+
})
|
|
574
709
|
|
|
575
|
-
return
|
|
576
|
-
}
|
|
710
|
+
return { api, View }
|
|
711
|
+
}
|
|
577
712
|
```
|
|
578
713
|
|
|
579
714
|
### Example: Coordinator calling explicit APIs
|
|
580
715
|
|
|
581
716
|
```ts
|
|
582
|
-
import type {
|
|
717
|
+
import type { UsersFeatureApi } from './users-feature'
|
|
583
718
|
|
|
584
719
|
type Coordinator = {
|
|
585
|
-
onUserSaved: () => void
|
|
720
|
+
onUserSaved: () => Promise<void>
|
|
586
721
|
}
|
|
587
722
|
|
|
588
|
-
export const createCoordinator = (users:
|
|
723
|
+
export const createCoordinator = (users: UsersFeatureApi): Coordinator => {
|
|
589
724
|
return {
|
|
590
|
-
onUserSaved: () => {
|
|
591
|
-
users.refreshUsers()
|
|
725
|
+
onUserSaved: async () => {
|
|
726
|
+
await users.refreshUsers()
|
|
592
727
|
},
|
|
593
728
|
}
|
|
594
729
|
}
|
|
595
730
|
```
|
|
596
731
|
|
|
732
|
+
### Example: Scoped invalidation by key
|
|
733
|
+
|
|
734
|
+
When a large list exists, invalidate only the affected rows.
|
|
735
|
+
|
|
736
|
+
```ts
|
|
737
|
+
import { component, div, type Handle } from '@vanijs/vani'
|
|
738
|
+
|
|
739
|
+
const rowHandles = new Map<string, Handle>()
|
|
740
|
+
|
|
741
|
+
export const bindUserRow = (id: string, handle: Handle) => {
|
|
742
|
+
rowHandles.set(id, handle)
|
|
743
|
+
return () => rowHandles.delete(id)
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export const invalidateUserRow = (id: string) => {
|
|
747
|
+
rowHandles.get(id)?.update()
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
export const UserRow = component<{ id: string; name: string }>((props, handle) => {
|
|
751
|
+
handle.effect(() => bindUserRow(props.id, handle))
|
|
752
|
+
return () => div(props.name)
|
|
753
|
+
})
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
### Explicit batching (optional)
|
|
757
|
+
|
|
758
|
+
If you dispatch many invalidations in a single tick, queue them and update once per handle.
|
|
759
|
+
|
|
760
|
+
```ts
|
|
761
|
+
import type { Handle } from '@vanijs/vani'
|
|
762
|
+
|
|
763
|
+
const pending = new Set<Handle>()
|
|
764
|
+
let scheduled = false
|
|
765
|
+
|
|
766
|
+
export const queueUpdate = (handle: Handle) => {
|
|
767
|
+
pending.add(handle)
|
|
768
|
+
if (scheduled) return
|
|
769
|
+
scheduled = true
|
|
770
|
+
queueMicrotask(() => {
|
|
771
|
+
scheduled = false
|
|
772
|
+
for (const item of pending) item.update()
|
|
773
|
+
pending.clear()
|
|
774
|
+
})
|
|
775
|
+
}
|
|
776
|
+
```
|
|
777
|
+
|
|
597
778
|
### Challenges with manual invalidation at scale
|
|
598
779
|
|
|
599
|
-
- Update fan
|
|
780
|
+
- Update fan-out: one action may need to notify several modules; keep this explicit via a
|
|
600
781
|
coordinator instead of hidden subscriptions.
|
|
601
|
-
- Over
|
|
602
|
-
(
|
|
603
|
-
-
|
|
604
|
-
|
|
605
|
-
-
|
|
606
|
-
|
|
782
|
+
- Over-invalidating: calling `handle.update()` too broadly can re-render large subtrees; prefer
|
|
783
|
+
small, keyed targets (row components, feature roots).
|
|
784
|
+
- Under-invalidating: missing an update call leaves views stale; treat "update after state change"
|
|
785
|
+
as part of the module contract and centralize commands.
|
|
786
|
+
- Ordering and race conditions: when multiple modules depend on shared data, update data first, then
|
|
787
|
+
invalidate in a predictable order; avoid interleaving async updates without coordination.
|
|
788
|
+
- Lifecycle leaks: if a handle isn't unsubscribed, updates keep firing; ensure `subscribe()` returns
|
|
789
|
+
a cleanup and is wired through `handle.effect()`.
|
|
790
|
+
- Debugging update paths: without implicit reactivity, you must trace who called `update()`. Keep
|
|
791
|
+
module APIs narrow, name update methods clearly (`refreshUsers`, `invalidateSearch`), and consider
|
|
792
|
+
instrumentation (log or wrap invalidation helpers).
|
|
607
793
|
|
|
608
794
|
Vani trades automatic coordination for transparency. In large apps, that means you should invest in
|
|
609
795
|
clear module boundaries, explicit cross-module APIs, and small invalidation targets.
|
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ This guarantees:
|
|
|
59
59
|
|
|
60
60
|
Vani requires:
|
|
61
61
|
|
|
62
|
-
-
|
|
62
|
+
- JS-first by default (optional JSX adapter)
|
|
63
63
|
- no compiler
|
|
64
64
|
- no build-time transforms
|
|
65
65
|
- no generated code
|
|
@@ -154,6 +154,24 @@ This separates:
|
|
|
154
154
|
|
|
155
155
|
---
|
|
156
156
|
|
|
157
|
+
## JSX (optional)
|
|
158
|
+
|
|
159
|
+
Vani is JS-first and transpiler-free, but it also ships a JSX adapter that maps JSX syntax to the
|
|
160
|
+
same runtime behavior (CSR + SSR).
|
|
161
|
+
|
|
162
|
+
TypeScript config example:
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"compilerOptions": {
|
|
167
|
+
"jsx": "react-jsx",
|
|
168
|
+
"jsxImportSource": "@vanijs/vani"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
157
175
|
## SSR (experimental)
|
|
158
176
|
|
|
159
177
|
Vani SSR is explicit and anchor-based. You call `renderToString`, and the output includes the same
|
|
@@ -185,11 +203,17 @@ Notes:
|
|
|
185
203
|
|
|
186
204
|
- ❌ Not a Virtual DOM
|
|
187
205
|
- ❌ Not reactive-by-default
|
|
188
|
-
- ❌ Not JSX-
|
|
206
|
+
- ❌ Not JSX-mandatory (optional adapter)
|
|
189
207
|
- ❌ Not compiler-driven
|
|
190
208
|
- ❌ Not a template language
|
|
191
209
|
- ❌ Not a framework that guesses intent
|
|
192
210
|
|
|
211
|
+
### Why not Web Components (yet)
|
|
212
|
+
|
|
213
|
+
Vani does not use Web Components today because the developer ergonomics are still rough and SSR
|
|
214
|
+
support is a key goal. We may revisit this if Web Components bring clear benefits without harming
|
|
215
|
+
productivity and cross-browser compatibility.
|
|
216
|
+
|
|
193
217
|
---
|
|
194
218
|
|
|
195
219
|
## Comparison with Popular Frameworks
|
|
@@ -206,7 +230,31 @@ Notes:
|
|
|
206
230
|
| SSR without heuristics | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
207
231
|
| Dependency-free core | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
208
232
|
|
|
209
|
-
⚠️ = partially / indirectly supported
|
|
233
|
+
⚠️ = partially / indirectly supported / average
|
|
234
|
+
|
|
235
|
+
The strength of Vani is its predictability and simplicity, while other frameworks focus on developer
|
|
236
|
+
productivity and ease of use, handling a lot of complexity behind the scenes automatically.
|
|
237
|
+
|
|
238
|
+
### Vani's Sweet Spot
|
|
239
|
+
|
|
240
|
+
✅ Perfect for:
|
|
241
|
+
|
|
242
|
+
- Dashboard widgets
|
|
243
|
+
- Micro-frontends
|
|
244
|
+
- Live-coding in the browser
|
|
245
|
+
- Embeddable components in other frameworks
|
|
246
|
+
- Performance-critical UIs where you need exact control
|
|
247
|
+
- Server-rendered sites
|
|
248
|
+
- Learning UI fundamentals (no magic, direct DOM)
|
|
249
|
+
- Lightweight SPAs or small Multi-Page Applications
|
|
250
|
+
|
|
251
|
+
❌ Not ideal for:
|
|
252
|
+
|
|
253
|
+
- Large, complex web applications with many interrelated states
|
|
254
|
+
- Teams that want framework conventions to handle complexity
|
|
255
|
+
- Projects needing a mature ecosystem
|
|
256
|
+
|
|
257
|
+
(at least not yet)
|
|
210
258
|
|
|
211
259
|
---
|
|
212
260
|
|