The DVE class compiles, renders, streams, and validates templates. One instance is reusable and stateless between calls, so you can create it once and share it.
For what goes inside the templates themselves, see Syntax and Expressions.
import DVE from '@neabyte/dve'
// Create one engine and reuse it across renders
const dve = new DVE()
// Render a template directly from its source text
dve.renderString('Hi {{ name }}.', { name: 'Ada' })
// returns 'Hi Ada.'
// Compile once, then render the same template with different data
const compiled = dve.compile('Hi {{ name }}.')
dve.render(compiled, { name: 'A' }) // returns 'Hi A.'
dve.render(compiled, { name: 'B' }) // returns 'Hi B.'Creates an engine. Every option is optional.
Every option is optional. The table is a summary, and the notes below explain the exact behaviour and what happens when a limit is crossed, so nothing is left to guesswork.
| Option | Type | Default | Summary |
|---|---|---|---|
resolveInclude |
(path: string) => string |
— | Loads the raw text for an include or a layout |
onEmit |
(event: DveEvent) => void |
— | Receives lifecycle events |
onChunk |
(chunk: string, name: string) => void |
— | Receives each streamed chunk |
maxIterations |
number |
100_000 |
Cap on one {{#each}} loop |
maxRenderIterations |
number |
1_000_000 |
Cap on all loop iterations in one render |
maxOutputSize |
number |
5_000_000 |
Cap on output length, counted in characters |
maxTemplateSize |
number |
1_000_000 |
Cap on one template source, counted in characters |
resolveInclude maps a template path such as partials/header.dve to its raw source string. It is called for both {{> path}} includes and {{< path}} layouts, and the path you receive is exactly the text written in the tag, so you decide how it maps to a file, a map entry, or anything else. When it is not set, rendering or validating a template that uses an include or a layout throws a ReferenceError. It runs synchronously, so load the source ahead of time rather than awaiting inside it.
onEmit receives a DveEvent for each lifecycle step, covering compile, validate, render, and error. See Events for the full shape. Leave it unset when you do not need observability, since the engine skips the timing work entirely in that case.
onChunk receives each non-empty chunk as it is produced during streaming, alongside the template name. It fires only on the renderStream and renderStringStream paths, not on the buffered string paths.
maxIterations bounds a single {{#each}} loop. When an array longer than this value is reached, the render throws a RangeError naming the offending count, before any of its items are emitted.
maxRenderIterations bounds the running total of loop iterations across the whole render, summed over every {{#each}} including nested loops and loops inside includes or layouts. One loop can stay under maxIterations while many loops together cross this ceiling, and crossing it throws a RangeError. Keep this at or above maxIterations, otherwise a single large loop trips the total cap first.
maxOutputSize bounds the rendered output length measured in characters, accumulated as each chunk is produced. The moment the running total passes the cap the render throws a RangeError, which stops a small template with a huge value from growing without bound.
maxTemplateSize bounds the length of one template source measured in characters, checked at compile time. A source longer than this throws a RangeError before parsing begins, and the same cap applies to every included or layout source resolved through resolveInclude.
const dve = new DVE({
resolveInclude: (path) => Deno.readTextFileSync(`views/${path}`),
maxTemplateSize: 50_000,
maxIterations: 1_000
})Note
Three nesting depths are each capped at 64 levels, the include and layout chain, the expression tree, and the block nesting. The include and layout chain throws a RangeError when it runs too deep (or a ReferenceError when an include has no resolver), while the expression and block caps are caught at parse time and throw a SyntaxError. Either way the engine stops rather than hanging. These caps are fixed and are not part of EngineOptions.
When onEmit is provided, the engine reports its lifecycle. Each event carries the template name as eventName.
eventKind |
Extra fields | Fired when |
|---|---|---|
view:compiled |
durationMs |
compile finishes parsing |
view:validated |
durationMs |
validate passes |
view:rendered |
durationMs |
a render completes |
view:error |
eventError: Error |
compile/validate/render throws |
const dve = new DVE({
onEmit: event => {
if (event.eventKind === 'view:error') {
console.error(event.eventName, event.eventError)
} else {
console.log(event.eventKind, event.eventName, event.durationMs)
}
}
})Parses template text into a reusable AST. Throws SyntaxError for malformed templates and RangeError if text exceeds maxTemplateSize. name (default '<template>') is only used for events and error messages.
const compiled = dve.compile('Hello {{ name }}.', 'greeting')Renders a compiled template to a string. data defaults to {}.
dve.render(compiled, { name: 'World' })
// returns 'Hello World.'Renders a compiled template to a UTF-8 ReadableStream, skipping empty chunks, which makes it a good fit for edge runtimes and Response bodies.
const stream = dve.renderStream(compiled, { name: 'World' })
return new Response(stream, {
headers: { 'content-type': 'text/html; charset=utf-8' }
})Compiles and renders in a single call, which is convenient for a one-off template.
dve.renderString('Hello {{ who }}.', { who: 'World' })
// returns 'Hello World.'Compiles and streams in a single call.
const stream = dve.renderStringStream('<h1>{{ title }}</h1>', { title: 'Hi' })Statically checks a compiled template: every expression parses, includes/layouts resolve, and each {{#block}} matches a layout {{#slot}}. Throws an aggregated Error listing all problems, otherwise returns void.
const dve = new DVE({ resolveInclude: path => layouts[path] })
dve.validate(dve.compile(pageSource, 'page'), 'page')Note
validate is meant to run at boot/CI time to catch broken templates before they ship, not on every request.