@niuxe/template-engine
v1.1.1
Published
Ultra-lightweight JavaScript template engine with automatic HTML escaping, intelligent caching, and optional plugins (~950 bytes gzipped)
Maintainers
Readme
TemplateEngine
Ultra-lightweight JavaScript template engine with automatic HTML escaping, intelligent caching, and optional plugins.
~950 bytes gzipped (core) | Zero dependencies | ES6+ | Modular
Why?
- Tiny: 27x smaller than Handlebars, 7x smaller than EJS, 3.4x smaller than Mustache.js
- Fast: Built-in compilation cache with LRU eviction
- Secure: Auto-escapes HTML by default
- Simple: Clean syntax, no build step required
- Modular: Optional plugins for partials, helpers, strict mode, and async file rendering
- Pay for what you use: Core is 950 bytes, add only the plugins you need
Installation
npm install @niuxe/template-engineimport { TemplateEngine } from '@niuxe/template-engine'Quick Start
const engine = new TemplateEngine()
const html = engine.render(`
<h1>[[= title ]]</h1>
<ul>
[[ items.forEach(item => { ]]
<li>[[= item.name ]] - $[[= item.price ]]</li>
[[ }) ]]
</ul>
`, {
title: 'Products',
items: [
{ name: 'Coffee', price: 3.50 },
{ name: 'Tea', price: 2.75 }
]
})Syntax
Output (escaped)
[[= variable ]]
[[= user.name ]]
[[= items[0] ]]Auto-escapes HTML entities (<, >, &, ", ')
Output (raw)
[[-htmlContent ]]Renders unescaped HTML (use with caution)
JavaScript Code
[[ if (user.admin) { ]]
<button>Admin Panel</button>
[[ } else { ]]
<button>Dashboard</button>
[[ } ]]
[[ items.forEach(item => { ]]
<div>[[= item ]]</div>
[[ }) ]]
[[ for (let i = 0; i < 10; i++) { ]]
<span>[[= i ]]</span>
[[ } ]]Plugins
TemplateEngine uses a modular plugin system. Import only what you need to keep your bundle small.
Partials Plugin (+200 bytes)
Reusable template fragments.
import { TemplateEngine } from '@niuxe/template-engine'
import { PartialsPlugin } from '@niuxe/template-engine/plugins/partials'
const engine = new TemplateEngine().use(PartialsPlugin)
engine.partial('header', '<header><h1>[[= title ]]</h1></header>')
engine.partial('footer', '<footer>© 2025</footer>')
const html = engine.render(`
[[> header ]]
<main>[[= content ]]</main>
[[> footer ]]
`, { title: 'My Site', content: 'Welcome!' })Syntax: [[> partialName ]]
Dynamic Partials Plugin (+450 bytes)
Select partials dynamically using variables - perfect for component libraries and conditional rendering.
import { TemplateEngine } from '@niuxe/template-engine'
import { PartialsPlugin } from '@niuxe/template-engine/plugins/partials'
import { DynamicPartialsPlugin } from '@niuxe/template-engine/plugins/partials-dynamic'
const engine = new TemplateEngine()
.use(PartialsPlugin)
.use(DynamicPartialsPlugin)
// Register different layouts
engine.partial('adminLayout', '<div class="admin">[[= content ]]</div>')
engine.partial('userLayout', '<div class="user">[[= content ]]</div>')
engine.partial('guestLayout', '<div class="guest">[[= content ]]</div>')
// Choose layout dynamically based on user role
const html = engine.render('[[> (layoutType) ]]', {
layoutType: 'adminLayout', // Variable determines which partial to use
content: 'Dashboard content'
})
// Works great in loops for rendering different component types
engine.partial('imageCard', '<div class="image">Image</div>')
engine.partial('videoCard', '<div class="video">Video</div>')
engine.partial('textCard', '<div class="text">Text</div>')
const items = engine.render(`
[[ items.forEach(item => { ]]
[[> (item.type) ]]
[[ }) ]]
`, {
items: [
{ type: 'imageCard' },
{ type: 'videoCard' },
{ type: 'textCard' }
]
})Syntax: [[> (variableName) ]]
Features:
- Supports dot notation:
[[> (user.preferences.theme) ]] - Works in loops and conditionals
- Graceful fallback for undefined variables (renders empty string)
Params Partials Plugin (+400 bytes)
Pass named parameters to partials - create reusable components with custom props.
import { TemplateEngine } from '@niuxe/template-engine'
import { PartialsPlugin } from '@niuxe/template-engine/plugins/partials'
import { ParamsPartialsPlugin } from '@niuxe/template-engine/plugins/partials-params'
const engine = new TemplateEngine()
.use(PartialsPlugin)
.use(ParamsPartialsPlugin)
// Create a reusable button component
engine.partial('button', `
<button class="btn btn-[[= variant ]] btn-[[= size ]]" type="[[= type ]]">
[[= label ]]
</button>
`)
// Use it with different parameters
const html = engine.render(`
[[> button variant="primary" size="large" type="submit" label="Save Changes" ]]
[[> button variant="secondary" size="small" type="button" label="Cancel" ]]
`, {})
// Create alert component
engine.partial('alert', `
<div class="alert alert-[[= type ]]" role="alert">
[[= message ]]
</div>
`)
const alerts = engine.render(`
[[> alert type="success" message="Operation successful!" ]]
[[> alert type="warning" message="Please be careful" ]]
[[> alert type="error" message="Something went wrong" ]]
`, {})Syntax: [[> partialName key1="value1" key2="value2" ]]
Features:
- Automatic type conversion:
"true"→true,"42"→42 - Parameters override context data
- Mix with global context variables
- Perfect for component libraries
Combining Dynamic + Params:
const engine = new TemplateEngine()
.use(PartialsPlugin)
.use(DynamicPartialsPlugin)
.use(ParamsPartialsPlugin)
// You can use both features together!
engine.partial('card', '<div class="[[= theme ]]">[[= title ]]</div>')
// Dynamic partial selection + parameters
engine.render('[[> (cardType) theme="dark" title="Hello" ]]', {
cardType: 'card'
})Helpers Plugin (+150 bytes)
Custom functions for formatting and transforming data.
import { TemplateEngine } from '@niuxe/template-engine'
import { HelpersPlugin } from '@niuxe/template-engine/plugins/helpers'
const engine = new TemplateEngine().use(HelpersPlugin)
engine.helper('uppercase', str => str.toUpperCase())
engine.helper('currency', price => `$${price.toFixed(2)}`)
const html = engine.render(`
<h1>[[= helpers.uppercase(title) ]]</h1>
<p>Price: [[= helpers.currency(price) ]]</p>
`, { title: 'hello', price: 19.99 })
// Output: <h1>HELLO</h1><p>Price: $19.99</p>Built-in helpers object: helpers.functionName(args)
Strict Mode Plugin (+290 bytes)
Throws errors when accessing undefined variables, helping catch typos and missing data.
import { TemplateEngine } from '@niuxe/template-engine'
import { StrictModePlugin } from '@niuxe/template-engine/plugins/strict'
const engine = new TemplateEngine().use(StrictModePlugin)
engine.strict = true
// ❌ Throws: Variable "userName" is not defined
engine.render('[[= userName ]]', { userNaem: 'John' })
// ✅ Works fine
engine.render('[[= userName ]]', { userName: 'John' })Perfect for catching refactoring errors and validating API responses.
I18n Plugin (+230 bytes)
Multi-language support with variable interpolation.
import { TemplateEngine } from '@niuxe/template-engine'
import { I18nPlugin } from '@niuxe/template-engine/plugins/i18n'
const engine = new TemplateEngine().use(I18nPlugin)
engine.translations = {
en: {
greeting: 'Hello {name}!',
items_count: 'You have {count} items'
},
fr: {
greeting: 'Bonjour {name} !',
items_count: 'Vous avez {count} articles'
}
}
// Switch language
engine.locale = 'fr'
const html = engine.render(`
<h1>[[= t("greeting", {name: userName}) ]]</h1>
<p>[[= t("items_count", {count: items.length}) ]]</p>
`, { userName: 'Alice', items: [1, 2, 3] })
// Output: <h1>Bonjour Alice !</h1><p>Vous avez 3 articles</p>Features:
- Variable interpolation with
{varName}syntax - Dynamic locale switching
- Fallback to key if translation missing
- Works with all template features (loops, conditionals)
Note: For complex i18n needs (plurals, dates, currencies), consider using i18next with the HelpersPlugin.
Async Plugin (+260 bytes)
Read and render templates from files (Node.js only).
import { TemplateEngine } from '@niuxe/template-engine'
import { AsyncPlugin } from '@niuxe/template-engine/plugins/async'
const engine = new TemplateEngine().use(AsyncPlugin)
// Read template from file system
const html = await engine.renderFile('./templates/email.html', {
name: 'Alice',
orderId: 12345
})Node.js only. Throws error in browser environments.
Combining Plugins
Plugins can be chained together:
import { TemplateEngine } from '@niuxe/template-engine'
import {
PartialsPlugin,
DynamicPartialsPlugin,
ParamsPartialsPlugin,
HelpersPlugin,
StrictModePlugin,
I18nPlugin
} from '@niuxe/template-engine/plugins'
const engine = new TemplateEngine()
.use(PartialsPlugin)
.use(DynamicPartialsPlugin)
.use(ParamsPartialsPlugin)
.use(HelpersPlugin)
.use(StrictModePlugin)
.use(I18nPlugin)
engine.strict = true
engine.locale = 'fr'
engine.partial('badge', '<span class="badge">[[= text ]]</span>')
engine.helper('upper', s => s.toUpperCase())
engine.translations = {
fr: { welcome: 'Bienvenue' }
}
const html = engine.render(`
[[> badge text="New" ]]
<p>[[= t("welcome") ]] [[= helpers.upper(name) ]]</p>
`, { name: 'alice' })Total size with all plugins: ~2.2 kio gzipped
Core API
render(template, data)
Compiles and renders a template with given data.
engine.render('<h1>[[= title ]]</h1>', { title: 'Hello' })
// Returns: '<h1>Hello</h1>'Parameters:
template(string): Template stringdata(object): Data object for interpolation
Returns: Rendered HTML string
Throws: Error if template is invalid or compilation fails
use(plugin)
Adds a plugin to the engine.
engine.use(PartialsPlugin)Returns: this (for chaining)
clear()
Clears the compilation cache.
engine.clear()Returns: this (for chaining)
Useful when:
- Updating partials or helpers
- Managing memory in long-running processes
- Testing
Advanced Examples
Component Library with Dynamic Partials
import { TemplateEngine } from '@niuxe/template-engine'
import { PartialsPlugin, DynamicPartialsPlugin, ParamsPartialsPlugin } from '@niuxe/template-engine/plugins'
const engine = new TemplateEngine()
.use(PartialsPlugin)
.use(DynamicPartialsPlugin)
.use(ParamsPartialsPlugin)
// Define components
engine.partial('button', '<button class="btn-[[= variant ]]">[[= label ]]</button>')
engine.partial('input', '<input type="[[= type ]]" placeholder="[[= placeholder ]]">')
engine.partial('card', '<div class="card-[[= theme ]]">[[= content ]]</div>')
// Render different components dynamically
const form = engine.render(`
[[ components.forEach(comp => { ]]
[[> (comp.type) variant="primary" label="Submit" ]]
[[ }) ]]
`, {
components: [
{ type: 'button' },
{ type: 'input' }
]
})Multi-state Component
engine.partial('loading', '<div class="spinner">Loading...</div>')
engine.partial('error', '<div class="error">[[= message ]]</div>')
engine.partial('success', '<div class="success">[[= data ]]</div>')
// Render based on application state
const widget = engine.render('[[> (state) message="Error occurred" data="Success!" ]]', {
state: 'loading' // Can be 'loading', 'error', or 'success'
})Email Template with Partials
engine.partial('header', `
<div style="background: #333; color: white; padding: 20px;">
<h1>[[= companyName ]]</h1>
</div>
`)
engine.partial('footer', `
<div style="text-align: center; color: #666;">
<p>© [[= year ]] [[= companyName ]]. All rights reserved.</p>
</div>
`)
const email = engine.render(`
[[> header ]]
<div style="padding: 20px;">
<p>Hi [[= userName ]],</p>
<p>Your order #[[= orderId ]] has been confirmed.</p>
<ul>
[[ items.forEach(item => { ]]
<li>[[= item.name ]] - [[= helpers.currency(item.price) ]]</li>
[[ }) ]]
</ul>
<p><strong>Total: [[= helpers.currency(total) ]]</strong></p>
</div>
[[> footer ]]
`, {
companyName: 'ACME Inc',
year: 2025,
userName: 'Alice',
orderId: 12345,
items: [
{ name: 'Product A', price: 29.99 },
{ name: 'Product B', price: 49.99 }
],
total: 79.98
})Performance
- Compilation cache: Templates are compiled once, cached for reuse
- Cache limit: 100 templates max (LRU eviction)
- Benchmarks (10,000 renders):
- First render: ~2ms (compilation + render)
- Cached renders: ~0.3ms (cache hit)
Security
HTML Escaping
By default, [[= ... ]] escapes HTML to prevent XSS:
engine.render('[[= html ]]', { html: '<script>alert("xss")</script>' })
// Returns: '<script>alert("xss")</script>'Raw Output
Use [[-... ]] for trusted HTML only:
engine.render('[[-trustedHTML ]]', { trustedHTML: '<b>Safe</b>' })
// Returns: '<b>Safe</b>'⚠️ Never use raw output with user-generated content.
Template Injection
Templates use JavaScript's with() statement and execute arbitrary code. Only use templates from trusted sources. Never allow users to submit their own template strings.
Use Strict Mode to catch undefined variables and prevent typos from becoming security issues.
Size Breakdown
| Component | Minified + Gzipped | |-----------|-------------------| | Core Engine | 950 bytes | | + Partials Plugin | +200 bytes | | + Dynamic Partials Plugin | +450 bytes | | + Params Partials Plugin | +400 bytes | | + Helpers Plugin | +150 bytes | | + Strict Mode Plugin | +290 bytes | | + Async Plugin | +260 bytes | | + I18n Plugin | +230 bytes | | All plugins combined | ~2.9 kio |
Comparison with alternatives
| Library | Size (gzipped) | Partials | Dynamic Partials | Parameters Partials | Helpers | I18n | Async | |---------|---------------|----------|------------------|------------|---------|------|-------| | TemplateEngine (core) | 950 bytes | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | TemplateEngine (full) | 2.9 kio | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Mustache.js | 3.2 kio | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | EJS | 4.3 kio | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | | Handlebars | 26 kio | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
TemplateEngine is:
- 3.4× lighter than Mustache.js (core: 950 bytes vs 3.2 kio)
- 7× lighter than EJS (core: 950 bytes vs 7 kio)
- 27× lighter than Handlebars (core: 950 bytes vs 26 kio)
- Same features as Handlebars for 9× less (full: 2.9 kio vs 26 kio)
Browser Support
Works in all modern browsers and Node.js 14+.
Requires:
- ES6 classes
- Private fields (
#) - Template literals
MapProxy(for Strict Mode plugin only)
Limitations
✅ What it does well:
- Small bundle size
- Fast rendering
- Simple syntax
- Plugin extensibility
- Component-based development (with Dynamic + Params plugins)
❌ What it doesn't do:
- No layout inheritance (use partials instead)
- No precompilation to static files
- No advanced i18n (plurals, date/currency formatting - use i18next instead)
- No sandboxing (templates can execute any JavaScript)
When to use:
- SPAs where bundle size matters
- Component libraries and design systems
- Simple server-side rendering
- Email templates
- Web components
When NOT to use:
- User-submitted templates (security risk)
- Complex CMS with untrusted content
- Need for advanced template inheritance
Migration from Handlebars
// Handlebars
{{> header}}
<h1>{{title}}</h1>
{{#each items}}
<li>{{name}}</li>
{{/each}}
// TemplateEngine (equivalent features, 14× smaller!)
[[> header ]]
<h1>[[= title ]]</h1>
[[ items.forEach(item => { ]]
<li>[[= item.name ]]</li>
[[ }) ]]
// Handlebars dynamic partials
{{> (whichPartial) }}
// TemplateEngine (with DynamicPartialsPlugin)
[[> (whichPartial) ]]
// Handlebars with parameters
{{> card title="Hello" theme="dark" }}
// TemplateEngine (with ParamsPartialsPlugin)
[[> card title="Hello" theme="dark" ]]Main differences:
{{}}→[[ ]]{{var}}→[[= var ]]{{{raw}}}→[[-raw ]]{{> name}}→[[> name ]](requires PartialsPlugin){{> (dynamic)}}→[[> (dynamic) ]](requires DynamicPartialsPlugin){{#each}}→[[ forEach ]](native JavaScript)
Contributing
Found a bug? Open an issue with a minimal reproduction.
Want to add a plugin? PRs welcome! Keep it small and focused.
License
MIT
Acknowledgments
Inspired by Handlebars, EJS, Underscore templates, and the pursuit of minimalism.
Built with ❤️ for developers who care about bundle size.
