Skip to content
RugvipPublic

About

A secure direct string template engine written in TypeScript

Resources

Security policy

Stars

3 stars

Watchers

1 watching

Forks

Repository files navigation

Nunjitsu

Nunjitsu is a secure native TypeScript template engine for direct template rendering. It supports a focused subset of Nunjucks 3.2.4 through a closed, synchronous interpreter rather than generated JavaScript.

Use Nunjitsu when templates may be untrusted and the application needs explicit control over every value and function exposed to them. The package targets Node.js 22 or newer and modern browsers, and accepts inline template source only; filesystem loading, precompilation, streaming, and asynchronous callbacks are outside its scope.

Use the Nunjucks templating documentation as the general syntax reference, then check the local compatibility guide for the subset supported by Nunjitsu. See the security model for trust-boundary details.

Installation

Install using your favorite package manager:

pnpm add nunjitsu
npm install nunjitsu
yarn add nunjitsu

Browser use

Import nunjitsu normally in a browser application. Bundlers that support the browser export condition select the browser ESM build automatically. The same synchronous TypeScript API works in both environments, without Node polyfills, precompilation, or permission to execute generated JavaScript.

The browser build targets ES2023 and requires Web Crypto for the random filter. See Security for browser-specific considerations.

TypeScript API

Quick start

Create a renderer once, then render templates synchronously:

import { createTemplateRenderer } from 'nunjitsu';

const renderer = createTemplateRenderer();

const output = renderer.render('Hello ${{ name }}!', {
  name: 'world',
});

console.log(output); // Hello world!

The default variable delimiters are ${{ and }}.

Creating a renderer

function createTemplateRenderer(
  options?: TemplateRendererOptions,
): TemplateRenderer;

createTemplateRenderer returns an immutable renderer. Its filters, globals, delimiter mode, and whitespace behavior cannot be changed after creation.

Option Type Default Description
filters Readonly<Record<string, TemplateFilter>> {} Trusted synchronous template filters.
globals Readonly<Record<string, TemplateGlobal>> {} Trusted values and synchronous functions.
allowRegexExecution boolean false Allows template-controlled regular-expression execution.
cookiecutterCompat boolean false Uses {{ and }} plus supported Jinja compatibility behavior.
trimBlocks boolean false Removes one newline immediately after block tags.
lstripBlocks boolean false Removes indentation before block tags on otherwise blank lines.

Rendering

interface TemplateRenderer {
  render(
    source: string,
    context?: TemplateContext | PreparedTemplateContext,
    options?: TemplateRenderOptions,
  ): string;
}

Each call parses and renders one complete template.

const result = renderer.render(
  '${{ user.name }} has ${{ items | length }} items',
  {
    user: { name: 'Patrik' },
    items: ['one', 'two'],
  },
);

Use renderValue when a sole interpolation should retain its value type:

interface TemplateRenderer {
  renderValue(
    source: string,
    context?: TemplateContext | PreparedTemplateContext,
    options?: TemplateRenderValueOptions,
  ): TemplateValue | undefined;
}

const config = renderer.renderValue('${{ config }}', {
  config: { retries: 3 },
});
// { retries: 3 }

Templates containing text, multiple interpolations, or statements return their normally rendered string. Native values are detached copies; returned arrays and records are frozen, while returned Maps and Sets remain independent mutable containers. This is useful for configuration, workflow, and structured-data templates.

To opt into JSON value coercion, pass { coercion: 'json' } to renderValue:

const condition = renderer.renderValue(
  '{% if enabled %}false{% endif %}',
  { enabled: true },
  { coercion: 'json' },
);
// false (boolean)

This mode is intended to match legacy Backstage Scaffolder value templating before its Nunjitsu migration. A single non-text root node can produce a JSON value; other root shapes return ordinary text. Empty output becomes undefined, while ${{ "" }} remains an empty string. The default is coercion: 'none', and render always keeps its ordinary behavior. See JSON value coercion for the rules and deliberate compatibility differences.

TemplateRenderOptions configures per-render resource limits. See Security for details.

Prepared contexts

Use a prepared context when the same data is rendered repeatedly. It is copied once into an immutable snapshot owned by the renderer:

interface TemplateRenderer {
  prepareContext(context?: TemplateContext): PreparedTemplateContext;
}

interface PreparedTemplateContext {
  withValue(
    path: readonly string[],
    value: TemplateValue,
  ): PreparedTemplateContext;
}

withValue returns a new snapshot and leaves the original unchanged.

const initial = renderer.prepareContext({
  parameters: { service: 'catalog' },
  steps: {},
});

const afterBuild = initial.withValue(['steps', 'build'], {
  output: { image: 'example/catalog:1.0' },
});

renderer.render('${{ steps.build.output.image }}', afterBuild);

A prepared context can only be used with the renderer that created it.

Template values

Contexts and capability results use plain data values:

type TemplateValue =
  | null
  | boolean
  | number
  | string
  | readonly TemplateValue[]
  | Readonly<{ [key: string]: TemplateValue }>
  | ReadonlyMap<TemplateValue, TemplateValue>
  | ReadonlySet<TemplateValue>;

type TemplateContext = Readonly<Record<string, TemplateValue>>;

Arrays, Maps, and Sets are copied before rendering. Approved template mutations preserve aliases during that render without modifying the caller's values or a prepared context.

Filters and globals

type TemplateFilter = (
  input: TemplateValue | undefined,
  ...arguments_: readonly (TemplateValue | undefined)[]
) => TemplateValue | undefined;

type TemplateGlobalFunction = (
  ...arguments_: readonly (TemplateValue | undefined)[]
) => TemplateValue | undefined;

type TemplateGlobal = TemplateValue | TemplateGlobalFunction;

Filters and global functions must be synchronous.

const renderer = createTemplateRenderer({
  filters: {
    slugify(input) {
      if (typeof input !== 'string') {
        throw new TypeError('slugify requires a string');
      }
      return input.toLowerCase().replaceAll(' ', '-');
    },
  },
  globals: {
    environment: 'production',
    deployment(name) {
      if (typeof name !== 'string') {
        throw new TypeError('deployment requires a string');
      }
      return { name, ready: true };
    },
  },
});

See Security for the capability boundary and failure behavior.

Development

Development requires Node.js 22.18 or newer and the pnpm version pinned in package.json.

pnpm install
pnpm test

Run pnpm benchmark for the full Nunjucks comparison harness. Architecture, testing, compatibility, and release documentation start in docs/. Contributors and coding agents must also follow AGENTS.md.

License

Licensed under the MIT License. Adapted Nunjucks tests retain their upstream license and attribution as documented in Compatibility.

About

A secure direct string template engine written in TypeScript

Resources

Security policy

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages