Skip to content

API ​

render() ​

Render a template string with data:

typescript
import { render } from 'janja';

const result = await render('Hello {{= name }}!', { name: 'World' });
// "Hello World!"

renderFile() ​

Render a template loaded through the loader option:

typescript
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:

typescript
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() ​

typescript
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() ​

typescript
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:

typescript
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 ​

typescript
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:

typescript
import { analyzeTemplate } from 'janja';

await analyzeTemplate('{{ for item of items }}{{= item.name | upper }}{{ endfor }}{{= title }}');
// {
//   variables: ['items', 'title'],
//   filters: ['upper'],
//   includes: [], blocks: [], macros: [],
// }

formatTemplate() ​

typescript
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:

typescript
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:

typescript
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.

Released under the MIT License.