voniq
v0.2.2
Published
๐ Fast, lightweight and modern template engine for Node.js
Readme
Install
npm install voniqFeatures
- Familiar syntax โ
{variable},{#if},{#each}, components - Zero dependencies, ESM-only, written in TypeScript
- Templates are compiled once into plain string builders โ no
eval - Renders after compilation are synchronous and allocation-light
- Compiled templates cached by path, concurrent-safe
- Components with props and children, resolved relative to the importing file
- Strict by default: missing variables fail loudly with helpful errors
- 100% test coverage
Quick start
templates/welcome.viq
Hello {user.name},
{#if items}
Order items:
{#each items as product}
- {product.name}: ${product.price}
{/each}
Total: ${total}
{#else}
Your cart is empty.
{/if}Render it:
import { render } from 'voniq'
const result = await render('templates/welcome.viq', {
user: { name: 'Ana' },
items: [
{ name: 'Keyboard', price: 199.9 },
{ name: 'Mouse', price: 89.5 }
],
total: 289.4
})Output:
Hello Ana,
Order items:
- Keyboard: $199.9
- Mouse: $89.5
Total: $289.4Syntax
Interpolation
Hello {username}
Hello { username } <!-- whitespace is trimmed -->
Hello {user.address.city} <!-- dot paths everywhere -->Missing variables throw a clear error instead of rendering silently empty:
voniq: missing variable "{username}"Conditionals
{#if premium}
Premium plan
{#else if trial}
Trial: {days} days left
{#else}
Free plan
{/if}Conditions are variable paths checked against template truthiness:
| Truthy | Falsy |
| ----------------- | ----------------- |
| 'text' | '' |
| 1 | 0, NaN |
| ['item'] | [] |
| {} | null, undefined |
| any object | false |
Empty lists are falsy, so {#if items} reads naturally. Missing variables evaluate to false instead of throwing.
Directives placed alone on a line don't leave blank lines behind:
<ul>
{#each users as user}
<li>{user.name}</li>
{/each}
</ul>Iteration
{#each users as user}
{index}. {user.name}
{/each}- The current item binds to
as alias(defaults toitem) {index}carries the loop position- Blocks nest freely; inner scopes see outer ones
Components
Define once in components/button.viq:
<a
href="{props.href}"
class="button button-{props.variant}"
>
{props.children}
</a>Import and invoke it anywhere:
{% import "components/button" %}
{#button href="{{url}}" variant="primary"}
Confirm your email
{/button}- Props are declared as attributes:
"text"is a literal,"{{path}}"binds a value from the calling scope - The content between the tags arrives as
{props.children} - Components without content can self-close:
{#footer year="{{y}}" /} - Component scope is isolated: only
propsis visible inside - Paths resolve relative to the importing file (
../../shared/xworks), extension.viqoptional - Circular imports are detected and reported
API
render(path, data?)
Reads, compiles and caches a template file, then renders it.
const html = await render('templates/activate.viq', {
url: 'https://example.dev/activate'
})Returns Promise<string>. Subsequent calls with the same path skip reading and compiling entirely.
compile(template)
Compiles a template string without touching the filesystem.
const renderFn = await compile('Hello {name}')
renderFn({ name: 'Ada' }) // 'Hello Ada'
renderFn({ name: 'Alan' }) // 'Hello Alan'Returns Promise<(data?: Vars) => string>. The returned function is synchronous โ all I/O happens once, during compilation.
Types
type Vars = Record<string, unknown>Errors at compile time
Broken templates fail when compiled, not in production renders:
voniq: unclosed "{#if}"
voniq: unexpected "{/each}"
voniq: duplicated "{#else}"
voniq: invalid binding "{{ }}" in prop "href"
voniq: circular import "b"
voniq: unknown component "{#button}"