| Crates.io | vegen |
| lib.rs | vegen |
| version | 0.5.0 |
| created_at | 2025-10-06 18:58:49.5226+00 |
| updated_at | 2025-10-23 18:54:50.601989+00 |
| description | A compiler for tiny, efficient, updatable TypeScript HTML templates |
| homepage | |
| repository | https://github.com/KMahoney/vegen |
| max_upload_size | |
| id | 1870647 |
| size | 2,071,017 |
VeGen is a compiler for tiny, efficient, updatable TypeScript HTML templates. A lower-level, meat-free alternative to view libraries like React.
VeGen is a compiler that takes HTML templates and generates TypeScript code (with types!) that builds the HTML and allows you to efficiently update it.
Instead of DOM diffing like React, VeGen compiles templates into small, easy to understand TypeScript objects that directly track any DOM nodes that may change.
The resulting TypeScript code contains a tiny library and is dependency-free, meaning it packs down very small. The examples app has ~4.5 kB of compressed Javascript in total.
Here's a simple counter example:
counter.vg:
<view name="Counter">
<div>
<h1>Counter example</h1>
<div>
<button onclick={clickHandler}>Clicked {count | numberToString} times</button>
</div>
</div>
</view>
main.ts:
import { Counter, run } from "./counter.ts";
const root = document.querySelector<HTMLDivElement>("#app")!;
root.append(
run(Counter, (get, set) => ({
clickHandler: () => {
set((s) => ({ ...s, count: s.count + 1 }));
},
count: 0,
}))
);
This generates a Counter function and CounterInput type, along with a run helper for managing component state.
The TypeScript generated (plus some additional comments) is:
export type CounterInput = {
clickHandler: (this: GlobalEventHandlers, ev: PointerEvent) => any;
count: number;
};
export function Counter(input: CounterInput): ViewState<CounterInput> {
// 't' is a helper for text nodes
// 'h' is a helper for DOM nodes
// Build the initial DOM:
const node0 = t(numberToString(input.count));
const node1 = h("button", { onclick: input.clickHandler }, [
t("Clicked "),
node0,
t(" times"),
]);
const root = h("div", {}, [
h("h1", {}, [t("Counter example")]),
h("div", {}, [node1]),
]);
// maintain state
let currentInput = input;
return {
// the root DOM element
root,
// the function to update the view
update(input) {
if (input.clickHandler !== currentInput.clickHandler) {
node1["onclick"] = input.clickHandler;
}
if (input.count !== currentInput.count) {
node0.textContent = numberToString(input.count);
}
currentInput = input;
},
};
}
cargo install vegen
brew install kmahoney/tap/vegen
Element and update function.VeGen generates highly efficient update code by tracking only the DOM nodes that may change in the ViewState. When you call the update function with new input, VeGen directly updates only the parts of the DOM that have actually changed.
However, to get the best performance, you need to be careful about reusing values from the previous state to avoid unnecessary re-renders. This is especially important with derived state.
The key principle is: if the data hasn't changed, pass the same object reference. VeGen can then skip updating that part of the DOM entirely.
This is particularly important for:
<for> loopsmap, filter, or similar operationsWhen you pass the same object reference, VeGen's update functions can quickly determine that no DOM changes are needed for that section.
Provide the vegen CLI command with .vg template files. Every view in every template will be compiled into TypeScript functions. Views can reference other views, including in other files.
A .vg template is a XML-like template that defines a series of views and can use several special forms. Each file consists of a series of view elements, e.g.
<view name="Example1">
view content
</view>
<view name="Example2">
view content
</view>
which will generate the TypeScript functions Example1, Example2 and their corresponding input types Example1Input, Example2Input.
VeGen supports expressions within {} bindings, including variables, function calls, pipes, and string templates.
Variables can be bound using simple names or dotted property paths:
<view name="UserProfile">
<h1>Welcome {user.name}!</h1>
<p>Age: {user.age}</p>
<p>Location: {user.address.city}, {user.address.country}</p>
</view>
Expressions can include function calls with arguments:
<view name="Formatted">
<div>Count: {numberToString(count)}</div>
<div>Price: {currency(amount, "USD")}</div>
</view>
They are useful for creating closures, such as binding event handlers in for loops:
<for seq={items} as="item">
<button onclick={clickItem(item.id)}>{item.name}</button>
</for>
where clickItem is (id) => (event) => void.
They are also useful for displaying data derived from the view inputs, but in this case you should make sure the output only changes when the inputs change, i.e. they're referentially transparent. Functions will be re-run when they are re-bound or their arguments change.
Built-in functions include:
boolean<T>(boolean, T, T) -> TnumberToString(number) -> stringlookup<T>(dict, key, default: T) -> TUse the pipe operator | to chain transformations:
<view name="Counter">
<div class="display">Count: {count | numberToString}</div>
<div>Status: {status | toUpperCase | prepend("Current: ")}</div>
</view>
Create dynamic strings with interpolation:
<view name="Greeting">
<p>{greeting}, {user.firstName} {user.lastName}!</p>
<p>Score: {"{points} / {total} ({percentage | formatPercent})"}</p>
</view>
Expressions can be nested and combined:
<view name="Advanced">
<div>{user.name | formatName("{first} {last}") | toUpperCase}</div>
<button onclick={handleClick(user.id, "edit")}>Edit {user.name}</button>
</view>
All expressions are statically typed and will infer appropriate TypeScript types for your input objects.
VeGen provides several special forms for control flow and dynamic content:
<if condition={showHeader}>
<then>
<h1>Welcome!</h1>
</then>
<else>
<p>Please log in</p>
</else>
</if>
This will conditionally show content and infer showHeader to be a boolean. The <else> block is optional.
<ul>
<for seq={todos} as="todo">
<li>Title: {todo.title}</li>
</for>
</ul>
This will loop through todos, introducing each element as the variable todo, and infer todos to be {title: string}[].
Render one of several branches based on a discriminant "type" field on a value.
<switch on={example}>
<case name="a">
<div>{a.foo}</div>
</case>
<case name="b">
<div>{b.bar}</div>
</case>
<case name="c">
<div>{c.baz | numberToString}</div>
</case>
</switch>
on expression must be a discriminated union with a string literal tag in a type field. For example:type Example =
| { type: "a"; foo: string }
| { type: "b"; bar: string }
| { type: "c"; baz: number };
VeGen supports composing views as reusable components within a template. Define multiple views in the same file, then use them as custom elements in parent views:
<view name="Button">
<button onclick={onClick} class={class}>{text}</button>
</view>
<view name="UserCard">
<div class="card">
<h3>{user.name}</h3>
<p>Age: {user.age | numberToString}</p>
<Button onClick={onEdit} class="btn-primary" text="Edit" />
</div>
</view>
<view name="UserList">
<div class="user-list">
<for seq={users} as="user">
<UserCard user={user} onEdit={editHandler(user.id)} />
</for>
</div>
</view>
Use <require src="..." /> at the top level of a template to pull in views defined in another .vg file. Required files are resolved relative to the current template, and all referenced views must be explicitly required. For example:
<require src="./components/header.vg" />
<view name="Page">
<Header title={title} />
</view>
You can also use a dynamically changing view with the 'use' form, as shown:
<use view={myView} attr={example} />
Where myView is a View<T> and T is the attribute object type.
run HelperThe generated TypeScript includes a run helper function that manages component state and provides reactive updates. It takes two parameters:
.vg template)update function for state managementrun(viewFunction, (update) => {
// Return the initial input state
return {
// ... your state properties
// ... event handlers that can call get() and set()
};
});
The update(currentState => newState) function updates the state and triggers a re-render of only the changed parts of the DOM.