API
render()
Render a template string with data:
import { render } from 'janja';
const result = await render('Hello {{= name }}!', { name: 'World' });
// "Hello World!"renderFile()
Render a template loaded through the loader option:
import { readFile } from 'node:fs/promises';
import { renderFile } from 'janja';
const options = { loader: (path) => readFile(`./templates/${path}.janja`, 'utf-8') };
const result = await renderFile('page', { name: 'World' }, options);The same loader resolves {{ include "..." }}. Without a loader, included templates are empty.
Renderer
render() and renderFile() reuse a renderer per options object, so pass the same object on every call to benefit from the compile cache, or create the renderer yourself:
import { Renderer } from 'janja';
const renderer = new Renderer({
// values available to every template
globals: { siteName: 'My Site' },
// HTML-escape output (default: true)
autoEscape: true,
// load templates for renderFile() and include
loader: async (path) => '...',
// receive errors instead of throwing them
debug: (error) => console.warn(error),
cache: {
enabled: true, // default
maxSize: 1000, // compiled templates to keep (default)
ttl: 3600000, // milliseconds (default: 1 hour), 0 to never expire
revalidate: true, // re-read included templates on every cache hit (default)
},
});
await renderer.render('Hello {{= name }} from {{= siteName }}!', { name: 'World' });Cache management: renderer.clearCache(), renderer.invalidateCache(predicate?), renderer.getCacheStats().
precompileTemplate() / renderCompiled()
import { precompileTemplate, renderCompiled } from 'janja';
// build time: returns the source of an ES module exporting `script`
const moduleSource = await precompileTemplate('Hello {{= name }}!', { loader });
// run time
import { script } from './hello.js';
await renderCompiled(script, { name: 'World' });validateTemplate()
import { validateTemplate } from 'janja';
const { valid, errors, warnings } = await validateTemplate('Hello {{= name }}');renderStream()
Render to a ReadableStream of strings, so the start of a page can be sent while the rest is still rendering:
import { renderStream } from 'janja';
const stream = renderStream(template, data);
// Node.js
for await (const chunk of stream) {
response.write(chunk);
}
// Fetch API / edge runtimes
return new Response(stream.pipeThrough(new TextEncoderStream()));Output is written between the top-level parts of a template and between the iterations of top-level loops (including loops inside a top-level if), in chunks of about 1,000 characters. Content inside capture, macro, block and included templates is produced as a whole. The stream respects backpressure, and cancelling it stops rendering. Errors reject the stream; in lenient mode they go to debug and the stream ends.
Typed templates
import { defineFilters, Renderer } from 'janja';
const renderer = new Renderer({
// argument types are checked where the filter is defined
filters: defineFilters({
shout: (value: string, times = 1) => value.toUpperCase() + '!'.repeat(times),
}),
});
// bind a template to the data it expects
const page = renderer.template<{ title: string; items?: string[] }>(source);
await page({ title: 'Hello' });For pre-compiled templates, janja compile --dts writes a declaration file per template with a TemplateData interface listing the data the template reads.
analyzeTemplate()
Statically determine what a template needs. Included templates are followed through the loader:
import { analyzeTemplate } from 'janja';
await analyzeTemplate('{{ for item of items }}{{= item.name | upper }}{{ endfor }}{{= title }}');
// {
// variables: ['items', 'title'],
// filters: ['upper'],
// includes: [], blocks: [], macros: [],
// }formatTemplate()
import { formatTemplate } from 'janja';
formatTemplate('<p>{{=name|upper}}</p>{{if a eq 1}}x{{endif}}');
// '<p>{{= name | upper }}</p>{{ if a eq 1 }}x{{ endif }}'Only the inside of tags changes; text, whitespace-control markers and multi-line comments are kept as they are. A tag is only rewritten when the result provably parses to the same expression.
createFileLoader()
A ready-made loader for Node.js that reads templates from a directory, with or without extension, and refuses paths that lead outside of it:
import { createFileLoader, Renderer } from 'janja';
const renderer = new Renderer({ loader: createFileLoader('./templates') });
await renderer.renderFile('pages/home', data);Source maps
precompile() returns the module together with a standard (version 3) source map pointing back into the template:
import { precompile } from 'janja';
const { code, map, dts } = await precompile(template, {
sourceMap: true,
filename: 'page.janja',
dts: true,
});janja compile --source-map and vite-plugin-janja use this, so stack traces and debuggers show template lines.