@vanijs/vani 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/DOCS.md +373 -0
- package/LICENSE +21 -0
- package/README.md +268 -0
- package/dist/lib/index.d.mts +240 -0
- package/dist/lib/index.mjs +1 -0
- package/llms.txt +8 -0
- package/package.json +89 -0
package/DOCS.md
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# Vani Documentation
|
|
2
|
+
|
|
3
|
+
Vani is a small, dependency‑free UI runtime built around a simple idea:
|
|
4
|
+
|
|
5
|
+
> Rendering should be explicit, local, and predictable.
|
|
6
|
+
|
|
7
|
+
Vani is **not** a Virtual DOM, not reactive‑by‑default, and not compiler‑driven. Components own a
|
|
8
|
+
DOM subtree delimited by anchors and only update when you explicitly ask them to.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add vani
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Quick Start (SPA)
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { component, div, button, renderToDOM, type Handle } from '@vanijs/vani'
|
|
24
|
+
|
|
25
|
+
const Counter = component((_, handle: Handle) => {
|
|
26
|
+
let count = 0
|
|
27
|
+
return () =>
|
|
28
|
+
div(
|
|
29
|
+
`Count: ${count}`,
|
|
30
|
+
button(
|
|
31
|
+
{
|
|
32
|
+
onclick: () => {
|
|
33
|
+
count += 1
|
|
34
|
+
handle.update()
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
'Inc',
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const appRoot = document.getElementById('app')
|
|
43
|
+
if (!appRoot) throw new Error('#app not found')
|
|
44
|
+
|
|
45
|
+
renderToDOM([Counter()], appRoot)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Core Concepts
|
|
51
|
+
|
|
52
|
+
### 1) Components are functions
|
|
53
|
+
|
|
54
|
+
Components are functions that return a render function:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { component, div } from '@vanijs/vani'
|
|
58
|
+
|
|
59
|
+
const Hello = component(() => {
|
|
60
|
+
return () => div('Hello Vani')
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2) Explicit updates
|
|
65
|
+
|
|
66
|
+
Nothing re‑renders unless you call `handle.update()`:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { component, div, button, type Handle } from '@vanijs/vani'
|
|
70
|
+
|
|
71
|
+
const Clicker = component((_, handle: Handle) => {
|
|
72
|
+
let clicks = 0
|
|
73
|
+
return () =>
|
|
74
|
+
div(
|
|
75
|
+
`Clicks: ${clicks}`,
|
|
76
|
+
button(
|
|
77
|
+
{
|
|
78
|
+
onclick: () => {
|
|
79
|
+
clicks += 1
|
|
80
|
+
handle.update()
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
'Click',
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 3) Subtree ownership (anchors)
|
|
90
|
+
|
|
91
|
+
Each component owns a DOM range delimited by anchors:
|
|
92
|
+
|
|
93
|
+
```html
|
|
94
|
+
<!--vani:start-->
|
|
95
|
+
... subtree ...
|
|
96
|
+
<!--vani:end-->
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Updates replace only the DOM between anchors.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## API Reference (with examples)
|
|
104
|
+
|
|
105
|
+
### `component(fn)`
|
|
106
|
+
|
|
107
|
+
Creates a component factory. The `fn` receives `props` and a `handle`.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { component, div, type Handle } from '@vanijs/vani'
|
|
111
|
+
|
|
112
|
+
const Card = component<{ title: string }>((props, handle: Handle) => {
|
|
113
|
+
handle.effect(() => {
|
|
114
|
+
console.log('Mounted:', props.title)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
return () => div(props.title)
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Components can return other component instances directly:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { component } from '@vanijs/vani'
|
|
125
|
+
import * as h from 'vani/html'
|
|
126
|
+
|
|
127
|
+
const Hero = component(() => {
|
|
128
|
+
return () => h.h1('Hello')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const Page = component(() => {
|
|
132
|
+
return () => Hero()
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### `renderToDOM(components, root)`
|
|
137
|
+
|
|
138
|
+
Mounts components to the DOM immediately.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { renderToDOM, component, div } from '@vanijs/vani'
|
|
142
|
+
|
|
143
|
+
const App = component(() => () => div('App'))
|
|
144
|
+
renderToDOM([App()], document.getElementById('app')!)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `hydrateToDOM(components, root)`
|
|
148
|
+
|
|
149
|
+
Binds handles to existing DOM (SSR/SSG) without rendering. You must call `handle.update()` to
|
|
150
|
+
activate.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { hydrateToDOM } from '@vanijs/vani'
|
|
154
|
+
import { App } from './app'
|
|
155
|
+
|
|
156
|
+
const root = document.getElementById('app')!
|
|
157
|
+
const handles = hydrateToDOM([App()], root)
|
|
158
|
+
handles.forEach((handle) => handle.update())
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `renderToString(components)`
|
|
162
|
+
|
|
163
|
+
Server‑side render to HTML with anchors. Import from `vani/ssr`.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { component } from '@vanijs/vani'
|
|
167
|
+
import { renderToString } from 'vani/ssr'
|
|
168
|
+
|
|
169
|
+
const App = component(() => () => 'Hello SSR')
|
|
170
|
+
const html = await renderToString([App()])
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### `mount(component, props)`
|
|
174
|
+
|
|
175
|
+
Low‑level helper for embedding raw component functions in the middle of a render tree. Use it when
|
|
176
|
+
you have an unwrapped component function.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { component, mount, div, type Component } from '@vanijs/vani'
|
|
180
|
+
|
|
181
|
+
const Footer: Component = () => () => div('Footer')
|
|
182
|
+
|
|
183
|
+
const Page = component(() => () => div('Body', mount(Footer, {})))
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### `fragment(...children)`
|
|
187
|
+
|
|
188
|
+
Returns a fragment node. Useful for returning multiple siblings.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { fragment, div, component } from '@vanijs/vani'
|
|
192
|
+
|
|
193
|
+
const App = component(() => () => fragment(div('One'), div('Two')))
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Element helpers
|
|
197
|
+
|
|
198
|
+
Vani ships helpers for common elements:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { div, span, button, input } from '@vanijs/vani'
|
|
202
|
+
|
|
203
|
+
div(span('Label'), input({ type: 'text' }), button({ onclick: () => {} }, 'Submit'))
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### `classNames(...classes)`
|
|
207
|
+
|
|
208
|
+
Utility for composing class names:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import { classNames, div } from '@vanijs/vani'
|
|
212
|
+
|
|
213
|
+
div({
|
|
214
|
+
className: classNames('base', { active: true }, ['p-2', 'rounded']),
|
|
215
|
+
})
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Refs
|
|
219
|
+
|
|
220
|
+
DOM refs and component refs are supported:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import { component, input, type DomRef, type ComponentRef } from '@vanijs/vani'
|
|
224
|
+
|
|
225
|
+
const Child = component((_, handle) => () => input({ ref: { current: null } }))
|
|
226
|
+
|
|
227
|
+
const Parent = component(() => {
|
|
228
|
+
const childRef: ComponentRef = { current: null }
|
|
229
|
+
return () => Child({ ref: childRef })
|
|
230
|
+
})
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Cleanup and effects
|
|
234
|
+
|
|
235
|
+
Effects are explicit and can return a cleanup function.
|
|
236
|
+
|
|
237
|
+
If you plan to use vani for a SSR/SSG application, you should use effects to run client-only code
|
|
238
|
+
such as accessing the window object, accessing the DOM, etc.
|
|
239
|
+
|
|
240
|
+
Effects are very simple, they don't have dependencies and are run once on mount and once on update.
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import { component, div } from '@vanijs/vani'
|
|
244
|
+
|
|
245
|
+
const Timer = component((_, handle) => {
|
|
246
|
+
handle.effect(() => {
|
|
247
|
+
const id = setInterval(() => console.log('tick'), 1000)
|
|
248
|
+
return () => clearInterval(id)
|
|
249
|
+
})
|
|
250
|
+
return () => div('Timer running…')
|
|
251
|
+
})
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Transitions
|
|
255
|
+
|
|
256
|
+
`startTransition` marks a group of updates as non-urgent, so they are deferred and batched
|
|
257
|
+
separately.
|
|
258
|
+
|
|
259
|
+
This is useful to avoid blocking the UI while expensive work happens, e.g. if you are filtering a
|
|
260
|
+
large list.
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
import { component, button, div, startTransition, type Handle } from '@vanijs/vani'
|
|
264
|
+
|
|
265
|
+
const List = component((_, handle: Handle) => {
|
|
266
|
+
let items = [1, 2, 3]
|
|
267
|
+
return () =>
|
|
268
|
+
div(
|
|
269
|
+
button(
|
|
270
|
+
{
|
|
271
|
+
onclick: () => {
|
|
272
|
+
startTransition(() => {
|
|
273
|
+
items = items.slice().reverse()
|
|
274
|
+
handle.update()
|
|
275
|
+
})
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
'Reverse',
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
})
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Async Components
|
|
287
|
+
|
|
288
|
+
Components may return a Promise of a render function. You can provide a `fallback`:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
import { component, div } from '@vanijs/vani'
|
|
292
|
+
|
|
293
|
+
const AsyncCard = component(async () => {
|
|
294
|
+
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
295
|
+
return () => div('Loaded')
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
const App = component(
|
|
299
|
+
() => () =>
|
|
300
|
+
AsyncCard({
|
|
301
|
+
fallback: () => div('Loading…'),
|
|
302
|
+
}),
|
|
303
|
+
)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
They are awaited and the fallback is rendered until the component is ready.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## clientOnly Islands
|
|
311
|
+
|
|
312
|
+
Use `clientOnly: true` to skip SSR of a component and render it only on the client:
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
import { component, div } from '@vanijs/vani'
|
|
316
|
+
|
|
317
|
+
const ClientWidget = component(() => () => div('Client‑only'))
|
|
318
|
+
|
|
319
|
+
const App = component(
|
|
320
|
+
() => () =>
|
|
321
|
+
ClientWidget({
|
|
322
|
+
clientOnly: true,
|
|
323
|
+
}),
|
|
324
|
+
)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## SSR, SSG, and SPA
|
|
330
|
+
|
|
331
|
+
Vani supports all three patterns:
|
|
332
|
+
|
|
333
|
+
### SPA
|
|
334
|
+
|
|
335
|
+
Use `renderToDOM()` to mount immediately on the client.
|
|
336
|
+
|
|
337
|
+
### SSR
|
|
338
|
+
|
|
339
|
+
Use `renderToString()` on the server, then `hydrateToDOM()` on the client.
|
|
340
|
+
|
|
341
|
+
### SSG
|
|
342
|
+
|
|
343
|
+
Use `renderToString()` at build time to generate a static `index.html`, then hydrate on the client.
|
|
344
|
+
|
|
345
|
+
**Important:** Hydration only binds to anchors. It does not render or start effects. Call
|
|
346
|
+
`handle.update()` to activate the UI.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Hydration Warnings
|
|
351
|
+
|
|
352
|
+
In dev mode, Vani warns about structural mismatches only:
|
|
353
|
+
|
|
354
|
+
- Missing `<!--vani:start-->` or `<!--vani:end-->`
|
|
355
|
+
- Anchor order mismatch (end before start)
|
|
356
|
+
- Unused anchors after hydration
|
|
357
|
+
|
|
358
|
+
This avoids slow DOM diffing and keeps behavior explicit.
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Philosophy Summary
|
|
363
|
+
|
|
364
|
+
- Explicit updates only (`handle.update()`)
|
|
365
|
+
- Localized DOM updates (subtree‑only)
|
|
366
|
+
- No virtual DOM, no diffing, no magic
|
|
367
|
+
- Hydration is binding, not execution
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## License
|
|
372
|
+
|
|
373
|
+
MIT
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Javier Aguilar (itsjavi.com)
|
|
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,268 @@
|
|
|
1
|
+
# Vani
|
|
2
|
+
|
|
3
|
+
**Vani** is a small, dependency-free UI runtime built around a simple idea:
|
|
4
|
+
|
|
5
|
+
> **Rendering should be explicit, local, and predictable.**
|
|
6
|
+
|
|
7
|
+
Vani is not a Virtual DOM, not reactive-by-default, and not compiler-driven.
|
|
8
|
+
It is a **runtime-first**, **DOM-anchored**, **subtree-based** rendering system designed for
|
|
9
|
+
clarity, performance, and long-term maintainability.
|
|
10
|
+
|
|
11
|
+
Docs: [DOCS.md](https://github.com/itsjavi/vani/blob/main/DOCS.md)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Philosophy
|
|
16
|
+
|
|
17
|
+
### 1. Explicit intent over implicit magic
|
|
18
|
+
|
|
19
|
+
In Vani, **nothing re-renders unless you explicitly ask for it**.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
handle.update()
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
That single call is the only thing that triggers a re-render.
|
|
26
|
+
|
|
27
|
+
There are:
|
|
28
|
+
|
|
29
|
+
- no dependency graphs
|
|
30
|
+
- no signals auto-tracking
|
|
31
|
+
- no state subscriptions
|
|
32
|
+
- no implicit invalidation
|
|
33
|
+
|
|
34
|
+
This makes performance characteristics **obvious and predictable**.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
### 2. Subtree ownership, not tree reconciliation
|
|
39
|
+
|
|
40
|
+
Each component owns **exactly one DOM range**, delimited by stable anchors:
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<!--vani:start-->
|
|
44
|
+
subtree
|
|
45
|
+
<!--vani:end-->
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Updates replace **only that subtree**, never parents or siblings.
|
|
49
|
+
|
|
50
|
+
This guarantees:
|
|
51
|
+
|
|
52
|
+
- leaf-only updates
|
|
53
|
+
- O(size of subtree) cost
|
|
54
|
+
- no accidental cascades
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
### 3. Runtime-first, not compiler-first
|
|
59
|
+
|
|
60
|
+
Vani requires:
|
|
61
|
+
|
|
62
|
+
- no JSX
|
|
63
|
+
- no compiler
|
|
64
|
+
- no build-time transforms
|
|
65
|
+
- no generated code
|
|
66
|
+
|
|
67
|
+
Components are plain TypeScript functions.
|
|
68
|
+
|
|
69
|
+
This keeps:
|
|
70
|
+
|
|
71
|
+
- debugging straightforward
|
|
72
|
+
- stack traces readable
|
|
73
|
+
- behavior inspectable at runtime
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### 4. Hydration is binding, not execution
|
|
78
|
+
|
|
79
|
+
During SSR hydration:
|
|
80
|
+
|
|
81
|
+
- **no setup runs**
|
|
82
|
+
- **no effects start**
|
|
83
|
+
- **no DOM is mutated**
|
|
84
|
+
|
|
85
|
+
Hydration only **binds runtime handles to existing DOM**. Actual execution happens on the first
|
|
86
|
+
explicit update.
|
|
87
|
+
|
|
88
|
+
This avoids:
|
|
89
|
+
|
|
90
|
+
- double subscriptions
|
|
91
|
+
- zombie timers
|
|
92
|
+
- mismatched server/client behavior
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### 5. Scheduling, not reactivity, handles performance
|
|
97
|
+
|
|
98
|
+
Performance is handled by:
|
|
99
|
+
|
|
100
|
+
- batching
|
|
101
|
+
- microtask queues
|
|
102
|
+
- transition scheduling
|
|
103
|
+
|
|
104
|
+
Not by reactive graphs or heuristics.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
startTransition(() => {
|
|
108
|
+
handle.update()
|
|
109
|
+
})
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This separates:
|
|
113
|
+
|
|
114
|
+
- **what** updates
|
|
115
|
+
- from **when** updates happen
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Core Design Principles
|
|
120
|
+
|
|
121
|
+
| Principle | Meaning |
|
|
122
|
+
| ----------------- | ----------------------------------------- |
|
|
123
|
+
| Explicit updates | Only `handle.update()` triggers rendering |
|
|
124
|
+
| Locality | Updates affect only the owning subtree |
|
|
125
|
+
| Determinism | Same inputs → same DOM mutations |
|
|
126
|
+
| No hidden work | No background tracking or diffing |
|
|
127
|
+
| Runtime clarity | Debuggable without tooling |
|
|
128
|
+
| Opt-in complexity | Advanced features are explicit |
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Goals
|
|
133
|
+
|
|
134
|
+
- Predictable performance at scale
|
|
135
|
+
- Leaf-only updates by default
|
|
136
|
+
- Zero runtime dependencies
|
|
137
|
+
- SSR without hydration heuristics
|
|
138
|
+
- Clear mental model for developers
|
|
139
|
+
- Long-term maintainability over convenience
|
|
140
|
+
- Web-standards-first
|
|
141
|
+
- ESM-first and designed to run in any modern environment
|
|
142
|
+
- Good and intuitive developer experience
|
|
143
|
+
- Reduce magic and complexity, give freedom back to the developers
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Ergonomic features
|
|
148
|
+
|
|
149
|
+
- Vani uses real HTML attribute names, with a small set of library-specific exceptions (`ref`,
|
|
150
|
+
`key`, `fallback`, `clientOnly`)
|
|
151
|
+
- Async components are supported, including fallbacks
|
|
152
|
+
- `className` accepts string, array, and object forms for ergonomic composition
|
|
153
|
+
- ESM-first and designed to run in any modern environment
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## SSR (experimental)
|
|
158
|
+
|
|
159
|
+
Vani SSR is explicit and anchor-based. You call `renderToString`, and the output includes the same
|
|
160
|
+
`<!--vani:start-->` / `<!--vani:end-->` anchors used by the client runtime.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { renderToString } from './vani/ssr'
|
|
164
|
+
import { hydrateToDOM } from './vani/runtime'
|
|
165
|
+
import { getHydrationChildren } from './demos/hydration-app'
|
|
166
|
+
|
|
167
|
+
const html = await renderToString(getHydrationChildren())
|
|
168
|
+
// inject `html` into <div id="app"> on the server
|
|
169
|
+
|
|
170
|
+
// On the client
|
|
171
|
+
const root = document.getElementById('app')
|
|
172
|
+
const handles = hydrateToDOM(getHydrationChildren(), root!)
|
|
173
|
+
handles.forEach((handle) => handle.update())
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Notes:
|
|
177
|
+
|
|
178
|
+
- SSR does not use a virtual DOM or heuristics.
|
|
179
|
+
- Hydration binds to anchors only; it does not render.
|
|
180
|
+
- If SSR output does not match the client component tree, Vani warns (dev only).
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## What Vani is NOT
|
|
185
|
+
|
|
186
|
+
- ❌ Not a Virtual DOM
|
|
187
|
+
- ❌ Not reactive-by-default
|
|
188
|
+
- ❌ Not JSX-based
|
|
189
|
+
- ❌ Not compiler-driven
|
|
190
|
+
- ❌ Not a template language
|
|
191
|
+
- ❌ Not a framework that guesses intent
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Comparison with Popular Frameworks
|
|
196
|
+
|
|
197
|
+
| Feature / Framework | Vani | React | Vue | Svelte | Solid |
|
|
198
|
+
| ---------------------- | ---- | ----- | --- | ------ | ----- |
|
|
199
|
+
| Virtual DOM | ❌ | ✅ | ✅ | ❌ | ❌ |
|
|
200
|
+
| Implicit reactivity | ❌ | ⚠️ | ✅ | ✅ | ✅ |
|
|
201
|
+
| Compiler required | ❌ | ❌ | ❌ | ✅ | ❌ |
|
|
202
|
+
| JSX required | ❌ | ✅ | ❌ | ❌ | ❌ |
|
|
203
|
+
| Explicit updates | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
204
|
+
| Leaf-only updates | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
205
|
+
| Runtime-only | ✅ | ⚠️ | ⚠️ | ❌ | ⚠️ |
|
|
206
|
+
| SSR without heuristics | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
207
|
+
| Dependency-free core | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
208
|
+
|
|
209
|
+
⚠️ = partially / indirectly supported
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Mental Model
|
|
214
|
+
|
|
215
|
+
Think of Vani as:
|
|
216
|
+
|
|
217
|
+
> **“Manually invalidated, DOM-owned UI subtrees.”**
|
|
218
|
+
|
|
219
|
+
You decide:
|
|
220
|
+
|
|
221
|
+
- when something updates
|
|
222
|
+
- how much updates
|
|
223
|
+
- and why it updates
|
|
224
|
+
|
|
225
|
+
Nothing else happens behind your back.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Who Vani is for
|
|
230
|
+
|
|
231
|
+
Vani is a good fit if you value:
|
|
232
|
+
|
|
233
|
+
- full control over rendering
|
|
234
|
+
- predictable performance
|
|
235
|
+
- small runtimes
|
|
236
|
+
- explicit data flow
|
|
237
|
+
- SSR without complexity
|
|
238
|
+
- understanding your tools deeply
|
|
239
|
+
|
|
240
|
+
It is **not** optimized for:
|
|
241
|
+
|
|
242
|
+
- rapid prototyping
|
|
243
|
+
- beginners
|
|
244
|
+
- implicit magic
|
|
245
|
+
- large teams that rely on conventions
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Status
|
|
250
|
+
|
|
251
|
+
Vani is experimental and evolving. The core architecture is intentionally small and stable.
|
|
252
|
+
|
|
253
|
+
Expect:
|
|
254
|
+
|
|
255
|
+
- iteration
|
|
256
|
+
- refinement
|
|
257
|
+
- careful additions
|
|
258
|
+
|
|
259
|
+
Not:
|
|
260
|
+
|
|
261
|
+
- rapid feature creep
|
|
262
|
+
- breaking conceptual changes
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
//#region src/vani/runtime.d.ts
|
|
2
|
+
type SSRNode = {
|
|
3
|
+
type: 'element';
|
|
4
|
+
tag: string;
|
|
5
|
+
props: Record<string, any>;
|
|
6
|
+
children: SSRNode[];
|
|
7
|
+
} | {
|
|
8
|
+
type: 'text';
|
|
9
|
+
text: string;
|
|
10
|
+
} | {
|
|
11
|
+
type: 'comment';
|
|
12
|
+
text: string;
|
|
13
|
+
} | {
|
|
14
|
+
type: 'fragment';
|
|
15
|
+
children: SSRNode[];
|
|
16
|
+
} | {
|
|
17
|
+
type: 'component';
|
|
18
|
+
instance: ComponentInstance<any>;
|
|
19
|
+
};
|
|
20
|
+
type VNode = Node | SSRNode;
|
|
21
|
+
interface Handle {
|
|
22
|
+
/**
|
|
23
|
+
* Schedules a render for the component.
|
|
24
|
+
* This triggers a re-render on the next microtask.
|
|
25
|
+
*/
|
|
26
|
+
update(): void;
|
|
27
|
+
/**
|
|
28
|
+
* Flushes the component render.
|
|
29
|
+
* This triggers a re-render immediately.
|
|
30
|
+
*/
|
|
31
|
+
updateSync(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Disposes the component: removes the component from the DOM and runs all cleanup functions.
|
|
34
|
+
*/
|
|
35
|
+
dispose(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Adds a cleanup function that is called when the component is disposed.
|
|
38
|
+
*/
|
|
39
|
+
onCleanup(fn: () => void): void;
|
|
40
|
+
/**
|
|
41
|
+
* This is purely syntatic sugar, as it is basically the same as running the function
|
|
42
|
+
* on the setup phase and calling onCleanup to add a cleanup function.
|
|
43
|
+
*
|
|
44
|
+
* Using effects is necessary in SSR mode, for side effects to not run on the server
|
|
45
|
+
* (e.g. timers, subscriptions, DOM usage, etc.)
|
|
46
|
+
*
|
|
47
|
+
* Runs a side effect function when the component is mounted.
|
|
48
|
+
* The returning function may be a cleanup function that is called when the component is disposed.
|
|
49
|
+
*
|
|
50
|
+
*/
|
|
51
|
+
effect(fn: () => void | (() => void)): void;
|
|
52
|
+
}
|
|
53
|
+
type RenderFn = () => VChild;
|
|
54
|
+
type Component<Props = any> = (props: Props, handle: Handle) => RenderFn | Promise<RenderFn>;
|
|
55
|
+
type ComponentInstance<Props = any> = {
|
|
56
|
+
$$vani: 'component';
|
|
57
|
+
component: Component<Props>;
|
|
58
|
+
props: Props;
|
|
59
|
+
/**
|
|
60
|
+
* A key is used to identify the component when it is re-rendered.
|
|
61
|
+
* If a key is provided, the component will be re-rendered only if the key changes.
|
|
62
|
+
*/
|
|
63
|
+
key?: string | number;
|
|
64
|
+
/**
|
|
65
|
+
* A ref is used to get a reference to the component instance.
|
|
66
|
+
* The ref is set to the component instance when the component is mounted.
|
|
67
|
+
* The ref is set to null when the component is disposed.
|
|
68
|
+
*/
|
|
69
|
+
ref?: ComponentRef;
|
|
70
|
+
clientOnly?: boolean;
|
|
71
|
+
};
|
|
72
|
+
type ComponentInput<Props> = Props & {
|
|
73
|
+
key?: string | number;
|
|
74
|
+
ref?: ComponentRef;
|
|
75
|
+
};
|
|
76
|
+
type ComponentMetaProps = {
|
|
77
|
+
key?: string | number;
|
|
78
|
+
ref?: ComponentRef;
|
|
79
|
+
fallback?: RenderFn;
|
|
80
|
+
clientOnly?: boolean;
|
|
81
|
+
};
|
|
82
|
+
type VChild = VNode | ComponentInstance<any> | string | number | null | undefined | false;
|
|
83
|
+
type DataAttribute = `data-${string}`;
|
|
84
|
+
type HtmlProps<T extends keyof HTMLElementTagNameMap> = Partial<Omit<HTMLElementTagNameMap[T], 'children' | 'className' | 'style'>> & {
|
|
85
|
+
className?: ClassName;
|
|
86
|
+
style?: string;
|
|
87
|
+
ref?: DomRef<HTMLElementTagNameMap[T]>;
|
|
88
|
+
} & { [key in DataAttribute]?: string | number | boolean | undefined | null };
|
|
89
|
+
type ClassName = string | undefined | null | {
|
|
90
|
+
[key: string]: boolean | undefined | null;
|
|
91
|
+
} | ClassName[];
|
|
92
|
+
type ComponentRef = {
|
|
93
|
+
current: Handle | null;
|
|
94
|
+
};
|
|
95
|
+
type DomRef<T extends HTMLElement = HTMLElement> = {
|
|
96
|
+
current: T | null;
|
|
97
|
+
};
|
|
98
|
+
type RenderMode = 'dom' | 'ssr';
|
|
99
|
+
declare function component(fn: Component<void>): (props?: ComponentMetaProps) => ComponentInstance<void>;
|
|
100
|
+
declare function component<Props>(fn: Component<Props>): (props: Props & ComponentMetaProps) => ComponentInstance<Props>;
|
|
101
|
+
declare function withRenderMode<T>(mode: RenderMode, fn: () => T): T;
|
|
102
|
+
declare function getRenderMode(): RenderMode;
|
|
103
|
+
declare function renderToDOM(components: Array<Component<any> | ComponentInstance<any>>, root: HTMLElement): Handle[];
|
|
104
|
+
declare function isComponentInstance(child: VChild): child is ComponentInstance<any>;
|
|
105
|
+
declare function classNames(...classes: ClassName[]): string;
|
|
106
|
+
declare function el<E extends keyof HTMLElementTagNameMap>(tag: E, props?: HtmlProps<E> | VChild | null, ...children: VChild[]): VNode;
|
|
107
|
+
declare const fragment: (...children: VChild[]) => {
|
|
108
|
+
type: "fragment";
|
|
109
|
+
children: SSRNode[];
|
|
110
|
+
} | DocumentFragment;
|
|
111
|
+
declare function mount<Props>(component: Component<Props>, props: Props): VNode;
|
|
112
|
+
/**
|
|
113
|
+
* Marks all updates triggered inside the callback as a "transition".
|
|
114
|
+
*
|
|
115
|
+
* A transition represents non-urgent UI work that can be deferred
|
|
116
|
+
* to keep the application responsive.
|
|
117
|
+
*
|
|
118
|
+
* Updates scheduled inside `startTransition`:
|
|
119
|
+
* - do NOT block user interactions
|
|
120
|
+
* - are batched separately from urgent updates
|
|
121
|
+
* - may be flushed later (e.g. after the current event or during idle time)
|
|
122
|
+
*
|
|
123
|
+
* Transitions are NOT animations.
|
|
124
|
+
* They do not control how updates look, only *when* they are applied.
|
|
125
|
+
*
|
|
126
|
+
* Typical use cases:
|
|
127
|
+
* - Filtering or sorting large lists
|
|
128
|
+
* - Rendering expensive subtrees
|
|
129
|
+
* - Applying async results that are not immediately visible
|
|
130
|
+
*
|
|
131
|
+
* Example:
|
|
132
|
+
* ```ts
|
|
133
|
+
* button({
|
|
134
|
+
* onclick: () => {
|
|
135
|
+
* // urgent update
|
|
136
|
+
* setOpen(true)
|
|
137
|
+
* handle.update()
|
|
138
|
+
*
|
|
139
|
+
* // non-urgent update
|
|
140
|
+
* startTransition(() => {
|
|
141
|
+
* setItems(filter(items))
|
|
142
|
+
* handle.update()
|
|
143
|
+
* })
|
|
144
|
+
* },
|
|
145
|
+
* })
|
|
146
|
+
* ```
|
|
147
|
+
*
|
|
148
|
+
* If multiple transitions are triggered, they are automatically batched.
|
|
149
|
+
* Transition updates never interrupt urgent updates.
|
|
150
|
+
*/
|
|
151
|
+
declare function startTransition(fn: () => void): void;
|
|
152
|
+
declare function hydrateToDOM(components: Array<Component<any> | ComponentInstance<any>>, root: HTMLElement): Handle[];
|
|
153
|
+
declare function isDevMode(): boolean;
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/vani/html.d.ts
|
|
156
|
+
declare const div: (propsOrChild?: VChild | HtmlProps<"div">, ...children: VChild[]) => VNode;
|
|
157
|
+
declare const span: (propsOrChild?: VChild | HtmlProps<"span">, ...children: VChild[]) => VNode;
|
|
158
|
+
declare const ul: (propsOrChild?: VChild | HtmlProps<"ul">, ...children: VChild[]) => VNode;
|
|
159
|
+
declare const li: (propsOrChild?: VChild | HtmlProps<"li">, ...children: VChild[]) => VNode;
|
|
160
|
+
declare const ol: (propsOrChild?: VChild | HtmlProps<"ol">, ...children: VChild[]) => VNode;
|
|
161
|
+
declare const dl: (propsOrChild?: VChild | HtmlProps<"dl">, ...children: VChild[]) => VNode;
|
|
162
|
+
declare const dt: (propsOrChild?: VChild | HtmlProps<"dt">, ...children: VChild[]) => VNode;
|
|
163
|
+
declare const dd: (propsOrChild?: VChild | HtmlProps<"dd">, ...children: VChild[]) => VNode;
|
|
164
|
+
declare const main: (propsOrChild?: VChild | HtmlProps<"main">, ...children: VChild[]) => VNode;
|
|
165
|
+
declare const header: (propsOrChild?: VChild | HtmlProps<"header">, ...children: VChild[]) => VNode;
|
|
166
|
+
declare const footer: (propsOrChild?: VChild | HtmlProps<"footer">, ...children: VChild[]) => VNode;
|
|
167
|
+
declare const section: (propsOrChild?: VChild | HtmlProps<"section">, ...children: VChild[]) => VNode;
|
|
168
|
+
declare const article: (propsOrChild?: VChild | HtmlProps<"article">, ...children: VChild[]) => VNode;
|
|
169
|
+
declare const aside: (propsOrChild?: VChild | HtmlProps<"aside">, ...children: VChild[]) => VNode;
|
|
170
|
+
declare const nav: (propsOrChild?: VChild | HtmlProps<"nav">, ...children: VChild[]) => VNode;
|
|
171
|
+
declare const details: (propsOrChild?: VChild | HtmlProps<"details">, ...children: VChild[]) => VNode;
|
|
172
|
+
declare const summary: (propsOrChild?: VChild | HtmlProps<"summary">, ...children: VChild[]) => VNode;
|
|
173
|
+
declare const a: (propsOrChild?: VChild | HtmlProps<"a">, ...children: VChild[]) => VNode;
|
|
174
|
+
declare const button: (propsOrChild?: VChild | HtmlProps<"button">, ...children: VChild[]) => VNode;
|
|
175
|
+
declare const input: (propsOrChild?: VChild | HtmlProps<"input">, ...children: VChild[]) => VNode;
|
|
176
|
+
declare const output: (propsOrChild?: VChild | HtmlProps<"output">, ...children: VChild[]) => VNode;
|
|
177
|
+
declare const textarea: (propsOrChild?: VChild | HtmlProps<"textarea">, ...children: VChild[]) => VNode;
|
|
178
|
+
declare const select: (propsOrChild?: VChild | HtmlProps<"select">, ...children: VChild[]) => VNode;
|
|
179
|
+
declare const option: (propsOrChild?: VChild | HtmlProps<"option">, ...children: VChild[]) => VNode;
|
|
180
|
+
declare const optgroup: (propsOrChild?: VChild | HtmlProps<"optgroup">, ...children: VChild[]) => VNode;
|
|
181
|
+
declare const label: (propsOrChild?: VChild | HtmlProps<"label">, ...children: VChild[]) => VNode;
|
|
182
|
+
declare const form: (propsOrChild?: VChild | HtmlProps<"form">, ...children: VChild[]) => VNode;
|
|
183
|
+
declare const progress: (propsOrChild?: VChild | HtmlProps<"progress">, ...children: VChild[]) => VNode;
|
|
184
|
+
declare const meter: (propsOrChild?: VChild | HtmlProps<"meter">, ...children: VChild[]) => VNode;
|
|
185
|
+
declare const fieldset: (propsOrChild?: VChild | HtmlProps<"fieldset">, ...children: VChild[]) => VNode;
|
|
186
|
+
declare const legend: (propsOrChild?: VChild | HtmlProps<"legend">, ...children: VChild[]) => VNode;
|
|
187
|
+
declare const datalist: (propsOrChild?: VChild | HtmlProps<"datalist">, ...children: VChild[]) => VNode;
|
|
188
|
+
declare const figure: (propsOrChild?: VChild | HtmlProps<"figure">, ...children: VChild[]) => VNode;
|
|
189
|
+
declare const figcaption: (propsOrChild?: VChild | HtmlProps<"figcaption">, ...children: VChild[]) => VNode;
|
|
190
|
+
declare const img: (propsOrChild?: VChild | HtmlProps<"img">, ...children: VChild[]) => VNode;
|
|
191
|
+
declare const picture: (propsOrChild?: VChild | HtmlProps<"picture">, ...children: VChild[]) => VNode;
|
|
192
|
+
declare const source: (propsOrChild?: VChild | HtmlProps<"source">, ...children: VChild[]) => VNode;
|
|
193
|
+
declare const video: (propsOrChild?: VChild | HtmlProps<"video">, ...children: VChild[]) => VNode;
|
|
194
|
+
declare const audio: (propsOrChild?: VChild | HtmlProps<"audio">, ...children: VChild[]) => VNode;
|
|
195
|
+
declare const iframe: (propsOrChild?: VChild | HtmlProps<"iframe">, ...children: VChild[]) => VNode;
|
|
196
|
+
declare const embed: (propsOrChild?: VChild | HtmlProps<"embed">, ...children: VChild[]) => VNode;
|
|
197
|
+
declare const time: (propsOrChild?: VChild | HtmlProps<"time">, ...children: VChild[]) => VNode;
|
|
198
|
+
declare const mark: (propsOrChild?: VChild | HtmlProps<"mark">, ...children: VChild[]) => VNode;
|
|
199
|
+
declare const p: (propsOrChild?: VChild | HtmlProps<"p">, ...children: VChild[]) => VNode;
|
|
200
|
+
declare const h1: (propsOrChild?: VChild | HtmlProps<"h1">, ...children: VChild[]) => VNode;
|
|
201
|
+
declare const h2: (propsOrChild?: VChild | HtmlProps<"h2">, ...children: VChild[]) => VNode;
|
|
202
|
+
declare const h3: (propsOrChild?: VChild | HtmlProps<"h3">, ...children: VChild[]) => VNode;
|
|
203
|
+
declare const h4: (propsOrChild?: VChild | HtmlProps<"h4">, ...children: VChild[]) => VNode;
|
|
204
|
+
declare const h5: (propsOrChild?: VChild | HtmlProps<"h5">, ...children: VChild[]) => VNode;
|
|
205
|
+
declare const h6: (propsOrChild?: VChild | HtmlProps<"h6">, ...children: VChild[]) => VNode;
|
|
206
|
+
declare const code: (propsOrChild?: VChild | HtmlProps<"code">, ...children: VChild[]) => VNode;
|
|
207
|
+
declare const pre: (propsOrChild?: VChild | HtmlProps<"pre">, ...children: VChild[]) => VNode;
|
|
208
|
+
declare const blockquote: (propsOrChild?: VChild | HtmlProps<"blockquote">, ...children: VChild[]) => VNode;
|
|
209
|
+
declare const var_: (propsOrChild?: VChild | HtmlProps<"var">, ...children: VChild[]) => VNode;
|
|
210
|
+
declare const kbd: (propsOrChild?: VChild | HtmlProps<"kbd">, ...children: VChild[]) => VNode;
|
|
211
|
+
declare const samp: (propsOrChild?: VChild | HtmlProps<"samp">, ...children: VChild[]) => VNode;
|
|
212
|
+
declare const cite: (propsOrChild?: VChild | HtmlProps<"cite">, ...children: VChild[]) => VNode;
|
|
213
|
+
declare const dfn: (propsOrChild?: VChild | HtmlProps<"dfn">, ...children: VChild[]) => VNode;
|
|
214
|
+
declare const abbr: (propsOrChild?: VChild | HtmlProps<"abbr">, ...children: VChild[]) => VNode;
|
|
215
|
+
declare const small: (propsOrChild?: VChild | HtmlProps<"small">, ...children: VChild[]) => VNode;
|
|
216
|
+
declare const strong: (propsOrChild?: VChild | HtmlProps<"strong">, ...children: VChild[]) => VNode;
|
|
217
|
+
declare const em: (propsOrChild?: VChild | HtmlProps<"em">, ...children: VChild[]) => VNode;
|
|
218
|
+
declare const br: (propsOrChild?: VChild | HtmlProps<"br">, ...children: VChild[]) => VNode;
|
|
219
|
+
declare const hr: (propsOrChild?: VChild | HtmlProps<"hr">, ...children: VChild[]) => VNode;
|
|
220
|
+
declare const table: (propsOrChild?: VChild | HtmlProps<"table">, ...children: VChild[]) => VNode;
|
|
221
|
+
declare const caption: (propsOrChild?: VChild | HtmlProps<"caption">, ...children: VChild[]) => VNode;
|
|
222
|
+
declare const colgroup: (propsOrChild?: VChild | HtmlProps<"colgroup">, ...children: VChild[]) => VNode;
|
|
223
|
+
declare const col: (propsOrChild?: VChild | HtmlProps<"col">, ...children: VChild[]) => VNode;
|
|
224
|
+
declare const tbody: (propsOrChild?: VChild | HtmlProps<"tbody">, ...children: VChild[]) => VNode;
|
|
225
|
+
declare const thead: (propsOrChild?: VChild | HtmlProps<"thead">, ...children: VChild[]) => VNode;
|
|
226
|
+
declare const tfoot: (propsOrChild?: VChild | HtmlProps<"tfoot">, ...children: VChild[]) => VNode;
|
|
227
|
+
declare const tr: (propsOrChild?: VChild | HtmlProps<"tr">, ...children: VChild[]) => VNode;
|
|
228
|
+
declare const td: (propsOrChild?: VChild | HtmlProps<"td">, ...children: VChild[]) => VNode;
|
|
229
|
+
declare const th: (propsOrChild?: VChild | HtmlProps<"th">, ...children: VChild[]) => VNode;
|
|
230
|
+
declare const style: (propsOrChild?: VChild | HtmlProps<"style">, ...children: VChild[]) => VNode;
|
|
231
|
+
declare const script: (propsOrChild?: VChild | HtmlProps<"script">, ...children: VChild[]) => VNode;
|
|
232
|
+
declare const noscript: (propsOrChild?: VChild | HtmlProps<"noscript">, ...children: VChild[]) => VNode;
|
|
233
|
+
declare const template: (propsOrChild?: VChild | HtmlProps<"template">, ...children: VChild[]) => VNode;
|
|
234
|
+
declare const slot: (propsOrChild?: VChild | HtmlProps<"slot">, ...children: VChild[]) => VNode;
|
|
235
|
+
//#endregion
|
|
236
|
+
//#region src/vani/ssr.d.ts
|
|
237
|
+
type Renderable = Component<any> | ComponentInstance<any>;
|
|
238
|
+
declare function renderToString(components: Renderable[]): Promise<string>;
|
|
239
|
+
//#endregion
|
|
240
|
+
export { ClassName, Component, ComponentInput, ComponentInstance, ComponentRef, DataAttribute, DomRef, Handle, HtmlProps, RenderFn, SSRNode, VChild, VNode, a, abbr, article, aside, audio, blockquote, br, button, caption, cite, classNames, code, col, colgroup, component, datalist, dd, details, dfn, div, dl, dt, el, em, embed, fieldset, figcaption, figure, footer, form, fragment, getRenderMode, h1, h2, h3, h4, h5, h6, header, hr, hydrateToDOM, iframe, img, input, isComponentInstance, isDevMode, kbd, label, legend, li, main, mark, meter, mount, nav, noscript, ol, optgroup, option, output, p, picture, pre, progress, renderToDOM, renderToString, samp, script, section, select, slot, small, source, span, startTransition, strong, style, summary, table, tbody, td, template, textarea, tfoot, th, thead, time, tr, ul, var_, video, withRenderMode };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e){let t=e;return t.__vaniKeyed||=new Map,t.__vaniKeyed}function t(e){return t=>{let n,r,i=!1,a=t;if(t&&typeof t==`object`){let e=t;if(n=e.key,r=e.ref,i=e.clientOnly,`key`in e||`ref`in e){let{key:t,ref:n,clientOnly:r,...i}=e;a=i}}return{$$vani:`component`,component:e,props:a,key:n,ref:r,clientOnly:i}}}let n=`dom`;function r(e,t){let r=n;n=e;let i=t();return i&&typeof i.finally==`function`?i.finally(()=>{n=r}):(n=r,i)}function i(){return n}function a(e){return typeof e==`object`&&!!e&&`type`in e}function o(e){return a(e)&&e.type===`element`}function s(e){return a(e)&&e.type===`fragment`}function c(e){return n===`dom`?document.createElement(e):{type:`element`,tag:e,props:{},children:[]}}function l(e){return n===`dom`?document.createTextNode(e):{type:`text`,text:e}}function u(e,t){if(n===`dom`){e.appendChild(t);return}(o(e)||s(e))&&e.children.push(t)}function d(e,t){let n=e.nextSibling;for(;n&&n!==t;){let e=n.nextSibling,t=n;t.__vaniDomRef&&(t.__vaniDomRef.current=null),n.remove(),n=e}}function ee(e){if(e==null||e===!1)return document.createComment(`vani:empty`);if(m(e)){let t=document.createDocumentFragment(),n=f(e.component,h(e),t);return e.ref&&(e.ref.current=n),t}if(typeof e==`string`||typeof e==`number`)return document.createTextNode(String(e));if(e instanceof Node)return e;throw Error(`[vani] render returned an unsupported node type in DOM mode`)}function f(e,t,n){let r=[],i=!1,a,o,s=t?.clientOnly===!0;if(O){let e=A;A+=1,a=M(n,e),o=N(a,e)}else a=document.createComment(`vani:start`),o=document.createComment(`vani:end`),n.appendChild(a),n.appendChild(o);let c,l={update(){i||(S?w.has(l)||(T.add(l),D()):(T.delete(l),w.add(l),re()))},updateSync(){if(i)return;let e=a.parentNode;if(!e)return;d(a,o);let t=ee(c());e.insertBefore(t,o)},onCleanup(e){r.push(e)},dispose(){if(!i){i=!0,w.delete(l),T.delete(l);for(let e of r)e();r.length=0,d(a,o),a.remove(),o.remove(),c=(()=>document.createComment(`disposed`))}},effect(e){let t=e();typeof t==`function`&&r.push(t)}};if(O&&!s){let n=!1;return c=()=>{if(!n){n=!0;let r=e(t,l);c=r instanceof Promise?()=>document.createComment(`async`):r}return c()},l}let u=e(t,l);return u instanceof Promise?(c=t?.fallback||(()=>document.createComment(`vani:async`)),(!O||s)&&l.update(),u.then(e=>{i||(c=e,l.update())}),l):(c=u,(!O||s)&&l.update(),l)}function p(e,t){if(!t)throw Error(`[vani] root element not found`);let n=[];for(let r of e){if(typeof r==`function`){let e=f(r,{},t);n.push(e);continue}let e=f(r.component,h(r),t);n.push(e)}return n}function m(e){let t=typeof Node<`u`&&e instanceof Node;if(typeof e!=`object`||t)return!1;let n=e;return n.$$vani===`component`&&typeof n.component==`function`}function te(e){let t=typeof Node<`u`&&e instanceof Node;return typeof e==`object`&&!!e&&!t&&!m(e)}function h(e){return e.clientOnly?{...e.props??{},clientOnly:!0}:e.props}function g(t,r){if(n===`ssr`){for(let e of r)if(!(e==null||e===!1||e===void 0)){if(m(e)){u(t,{type:`component`,instance:e});continue}if(typeof e==`string`||typeof e==`number`){u(t,l(String(e)));continue}if(a(e)){u(t,e);continue}}return}let i=t;for(let t of r)if(!(t==null||t===!1||t===void 0)){if(m(t)){if(t.key!=null){let n=e(i),r=i.__vaniUsedKeys??=new Set,a=n.get(t.key);if(!a){let e=document.createDocumentFragment(),r=f(t.component,h(t),e);t.ref&&(t.ref.current=r),a={fragment:e,handle:r,ref:t.ref},n.set(t.key,a),t.ref&&(t.ref.current=r)}r.add(t.key),i.appendChild(a.fragment);continue}let n=document.createDocumentFragment(),r=f(t.component,h(t),n);t.ref&&(t.ref.current=r),i.appendChild(n);continue}if(typeof t==`string`||typeof t==`number`){i.appendChild(document.createTextNode(String(t)));continue}i.appendChild(t)}let o=i.__vaniKeyed,s=i.__vaniUsedKeys;if(o&&s){for(let[e,t]of o)s.has(e)||(t.handle.dispose(),t.ref&&(t.ref.current=null),o.delete(e));s.clear()}}function _(e,t){for(let n in t){let r=t[n];if(![`key`,`ref`].includes(n)){if(n===`className`){let t=v(r);o(e)?e.props.class=t:e.className=t;continue}if(n.startsWith(`on`)&&typeof r==`function`)o(e)||(e[n.toLowerCase()]=r);else if(r===!0)o(e)?e.props[n]=!0:e.setAttribute(n,``);else if(r===!1||r==null)continue;else o(e)?e.props[n]=String(r):e.setAttribute(n,String(r))}}}function v(...e){return e.map(e=>{if(!(e==null||e===``))return typeof e==`string`?e.trim():Array.isArray(e)?v(...e):Object.entries(e).filter(([e,t])=>t).map(([e])=>e.trim()).join(` `).trim()}).filter(Boolean).join(` `)}function y(e,t,...n){let r=c(e);return te(t)?(t.ref&&(o(r)?t.ref.current=null:(t.ref.current=r,r.__vaniDomRef=t.ref)),_(r,t),g(r,n),r):(g(r,[t,...n]),r)}const ne=(...e)=>{if(n===`ssr`){let t={type:`fragment`,children:[]};return g(t,e),t}let t=document.createDocumentFragment();return g(t,e),t};function b(e,t){if(n===`ssr`)return{type:`component`,instance:{$$vani:`component`,component:e,props:t}};let r=document.createDocumentFragment();return f(e,t,r),r}let x=!1,S=!1,C=!1;const w=new Set,T=new Set;function E(e){let t=S;S=!0;try{e()}finally{S=t,D()}}function D(){C||(C=!0,setTimeout(()=>{C=!1,ie()},0))}function re(){x||(x=!0,queueMicrotask(()=>{x=!1;for(let e of w)e.updateSync();w.clear()}))}function ie(){for(let e of T)e.updateSync();T.clear(),T.size>0&&D()}let O=!1,k=null,A=0;function j(e){F()&&console.warn(`[vani] hydration warning: ${e}`)}function M(e,t){let n=k;(!n||!e.contains(n))&&(n=e.firstChild);let r=!1;for(;n;){if(n.nodeType===Node.COMMENT_NODE&&n.nodeValue===`vani:start`)return r&&j(`Found <!--vani:end--> before <!--vani:start--> for component #${t}. This usually means the server HTML anchor order is incorrect.`),n;n.nodeType===Node.COMMENT_NODE&&n.nodeValue===`vani:end`&&(r=!0),n=n.nextSibling}throw j(`Expected <!--vani:start--> for component #${t}, but none was found. This usually means the server HTML does not match the client component tree.`),Error(`[vani] hydration failed: start anchor not found`)}function N(e,t){let n=e.nextSibling,r=0;for(;n;){if(n.nodeType===Node.COMMENT_NODE){if(n.nodeValue===`vani:start`)r+=1;else if(n.nodeValue===`vani:end`){if(r===0)return k=n.nextSibling,n;--r}}n=n.nextSibling}throw j(`Expected <!--vani:end--> for component #${t}, but none was found. This usually means the server HTML does not match the client component tree.`),Error(`[vani] hydration failed: end anchor not found`)}function P(e,t){let n=[];O=!0,k=t.firstChild,A=0;try{n=p(e,t)}catch(e){console.error(`[vani] hydration failed:`,e)}finally{if(F()&&k){let e=k,t=!1;for(;e;){if(e.nodeType===Node.COMMENT_NODE){let n=e.nodeValue;if(n===`vani:start`||n===`vani:end`){t=!0;break}}e=e.nextSibling}t&&j(`Unused SSR anchors detected after hydration. Some server-rendered DOM was not claimed by the client runtime.`)}O=!1,k=null,A=0}return n}function F(){return`__vaniDevMode`in globalThis?globalThis.__vaniDevMode===!0:import.meta.env?import.meta.env.DEV:typeof process<`u`&&process.env!==void 0?process.env.NODE_ENV===`development`:!1}function I(e){return(t,...n)=>y(e,t,...n)}const L=I(`div`),R=I(`span`),z=I(`ul`),B=I(`li`),V=I(`ol`),H=I(`dl`),U=I(`dt`),W=I(`dd`),G=I(`main`),K=I(`header`),q=I(`footer`),J=I(`section`),Y=I(`article`),ae=I(`aside`),oe=I(`nav`),se=I(`details`),ce=I(`summary`),le=I(`a`),ue=I(`button`),de=I(`input`),fe=I(`output`),pe=I(`textarea`),me=I(`select`),he=I(`option`),ge=I(`optgroup`),_e=I(`label`),ve=I(`form`),ye=I(`progress`),be=I(`meter`),xe=I(`fieldset`),Se=I(`legend`),Ce=I(`datalist`),we=I(`figure`),Te=I(`figcaption`),Ee=I(`img`),De=I(`picture`),Oe=I(`source`),ke=I(`video`),Ae=I(`audio`),je=I(`iframe`),Me=I(`embed`),Ne=I(`time`),Pe=I(`mark`),Fe=I(`p`),Ie=I(`h1`),Le=I(`h2`),Re=I(`h3`),ze=I(`h4`),Be=I(`h5`),Ve=I(`h6`),He=I(`code`),Ue=I(`pre`),We=I(`blockquote`),Ge=I(`var`),Ke=I(`kbd`),qe=I(`samp`),Je=I(`cite`),Ye=I(`dfn`),Xe=I(`abbr`),Ze=I(`small`),Qe=I(`strong`),$e=I(`em`),et=I(`br`),tt=I(`hr`),nt=I(`table`),rt=I(`caption`),it=I(`colgroup`),at=I(`col`),ot=I(`tbody`),st=I(`thead`),ct=I(`tfoot`),lt=I(`tr`),ut=I(`td`),dt=I(`th`),ft=I(`style`),pt=I(`script`),mt=I(`noscript`),ht=I(`template`),X=I(`slot`),gt=new Set([`area`,`base`,`br`,`col`,`embed`,`hr`,`img`,`input`,`link`,`meta`,`param`,`source`,`track`,`wbr`]);function _t(){return{update(){},updateSync(){},dispose(){},onCleanup(){},effect(){}}}function vt(e){return typeof e==`function`?{$$vani:`component`,component:e,props:{}}:e}function Z(e){return e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`)}function yt(e){let t=[];for(let n of Object.keys(e)){let r=e[n];r==null||r===!1||n.startsWith(`on`)&&typeof r==`function`||(r===!0?t.push(n):t.push(`${n}="${Z(String(r))}"`))}return t.length>0?` ${t.join(` `)}`:``}function Q(e){if(e==null||e===!1)return{type:`fragment`,children:[]};if(typeof e==`string`||typeof e==`number`)return{type:`text`,text:String(e)};if(m(e))return{type:`component`,instance:e};if(typeof e==`object`&&`type`in e)return e;throw Error(`[vani] SSR received a DOM node. This is not supported.`)}async function bt(e){let t=`<!--vani:start-->`,n=`<!--vani:end-->`;if(e.clientOnly){let r=e.props?.fallback;return r?`${t}${await $(Q(r()))}${n}`:`${t}${n}`}let r=e.component(e.props,_t());return`${t}${await $(Q((r instanceof Promise?await r:r)()))}${n}`}async function $(e){switch(e.type){case`text`:return Z(e.text);case`comment`:return`<!--${e.text}-->`;case`fragment`:return(await Promise.all(e.children.map($))).join(``);case`component`:return bt(e.instance);case`element`:{let t=yt(e.props);if(gt.has(e.tag))return`<${e.tag}${t}>`;let n=(await Promise.all(e.children.map($))).join(``);return`<${e.tag}${t}>${n}</${e.tag}>`}}}async function xt(e){return r(`ssr`,async()=>{if(i()!==`ssr`)throw Error(`[vani] renderToString failed to set SSR render mode.`);let t=e.map(e=>({type:`component`,instance:vt(e)}));return(await Promise.all(t.map($))).join(``)})}export{le as a,Xe as abbr,Y as article,ae as aside,Ae as audio,We as blockquote,et as br,ue as button,rt as caption,Je as cite,v as classNames,He as code,at as col,it as colgroup,t as component,Ce as datalist,W as dd,se as details,Ye as dfn,L as div,H as dl,U as dt,y as el,$e as em,Me as embed,xe as fieldset,Te as figcaption,we as figure,q as footer,ve as form,ne as fragment,i as getRenderMode,Ie as h1,Le as h2,Re as h3,ze as h4,Be as h5,Ve as h6,K as header,tt as hr,P as hydrateToDOM,je as iframe,Ee as img,de as input,m as isComponentInstance,F as isDevMode,Ke as kbd,_e as label,Se as legend,B as li,G as main,Pe as mark,be as meter,b as mount,oe as nav,mt as noscript,V as ol,ge as optgroup,he as option,fe as output,Fe as p,De as picture,Ue as pre,ye as progress,p as renderToDOM,xt as renderToString,qe as samp,pt as script,J as section,me as select,X as slot,Ze as small,Oe as source,R as span,E as startTransition,Qe as strong,ft as style,ce as summary,nt as table,ot as tbody,ut as td,ht as template,pe as textarea,ct as tfoot,dt as th,st as thead,Ne as time,lt as tr,z as ul,Ge as var_,ke as video,r as withRenderMode};
|
package/llms.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Vani project agent guide
|
|
2
|
+
|
|
3
|
+
Vani (from Vanilla) is a Web Standards-first framework for building web applications.
|
|
4
|
+
|
|
5
|
+
A fine-grained reactive open source framework for building lightning-fast apps — no virtual DOM, no
|
|
6
|
+
compiler, no JSX, no transpilation, zero dependencies, SSR support, imperative reactivity.
|
|
7
|
+
|
|
8
|
+
Check the DOCS.md and README.md files for more information.
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vanijs/vani",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Web-standards-first UI runtime with explicit updates, fine-grained DOM ownership, and zero dependencies. No Virtual DOM, no JSX, no compiler. Built for SPA, SSR, and SSG.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ui",
|
|
7
|
+
"frontend",
|
|
8
|
+
"framework",
|
|
9
|
+
"web-standards",
|
|
10
|
+
"dom",
|
|
11
|
+
"fine-grained",
|
|
12
|
+
"explicit-rendering",
|
|
13
|
+
"no-virtual-dom",
|
|
14
|
+
"no-jsx",
|
|
15
|
+
"no-compiler",
|
|
16
|
+
"ssr",
|
|
17
|
+
"ssg",
|
|
18
|
+
"hydration",
|
|
19
|
+
"signals",
|
|
20
|
+
"vanilla",
|
|
21
|
+
"vanilla-js",
|
|
22
|
+
"typescript",
|
|
23
|
+
"zero-dependencies"
|
|
24
|
+
],
|
|
25
|
+
"homepage": "https://itsjavi.com/vani",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/itsjavi/vani.git"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"author": {
|
|
32
|
+
"name": "Javier Aguilar"
|
|
33
|
+
},
|
|
34
|
+
"type": "module",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./dist/lib/index.d.mts",
|
|
38
|
+
"default": "./dist/lib/index.mjs"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"module": "./dist/lib/index.mjs",
|
|
42
|
+
"types": "./dist/lib/index.d.mts",
|
|
43
|
+
"files": [
|
|
44
|
+
"dist/lib/**/*",
|
|
45
|
+
"package.json",
|
|
46
|
+
"llms.txt",
|
|
47
|
+
"DOCS.md",
|
|
48
|
+
"README.md",
|
|
49
|
+
"LICENSE"
|
|
50
|
+
],
|
|
51
|
+
"dependencies": {},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@eslint/js": "^9.39.2",
|
|
54
|
+
"@shikijs/langs": "^3.21.0",
|
|
55
|
+
"@shikijs/themes": "^3.21.0",
|
|
56
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
57
|
+
"@types/bun": "^1.3.6",
|
|
58
|
+
"@types/node": "^25.0.9",
|
|
59
|
+
"eslint": "^9.39.2",
|
|
60
|
+
"globals": "^17.0.0",
|
|
61
|
+
"lucide-react": "^0.562.0",
|
|
62
|
+
"prettier": "^3.8.0",
|
|
63
|
+
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
64
|
+
"publint": "^0.3.16",
|
|
65
|
+
"shiki": "^3.21.0",
|
|
66
|
+
"tailwind-merge": "^3.4.0",
|
|
67
|
+
"tailwindcss": "^4.1.18",
|
|
68
|
+
"tsdown": "0.20.0-beta.4",
|
|
69
|
+
"tw-animate-css": "^1.4.0",
|
|
70
|
+
"typescript": "~5.9.3",
|
|
71
|
+
"typescript-eslint": "^8.53.1",
|
|
72
|
+
"vite": "npm:rolldown-vite@7.3.1"
|
|
73
|
+
},
|
|
74
|
+
"publishConfig": {
|
|
75
|
+
"access": "public"
|
|
76
|
+
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "rm -rf dist && vite build && pnpm run build:lib",
|
|
79
|
+
"build:lib": "pnpm tsdown src/vani/index.ts --minify --out-dir dist/lib --dts",
|
|
80
|
+
"dev": "vite",
|
|
81
|
+
"format": "prettier --write . && pnpm dlx sort-package-json",
|
|
82
|
+
"lint": "pnpm run typecheck && pnpm run lint:eslint && pnpm run lint:circular",
|
|
83
|
+
"lint:circular": "pnpm dlx madge --circular --extensions ts src/",
|
|
84
|
+
"lint:eslint": "eslint .",
|
|
85
|
+
"lint:unused": "pnpm dlx knip --exclude exports",
|
|
86
|
+
"preview": "vite preview",
|
|
87
|
+
"typecheck": "tsc --noEmit"
|
|
88
|
+
}
|
|
89
|
+
}
|