Skip to content

Migrating from Nunjucks

Runjucks targets the same mental model as Nunjucks: familiar {{ }} / {% %} syntax, Environment, configure, renderString, and composition with extends, include, import, and macros. The engine is a Rust tree-walker behind N-API instead of compile-to-JavaScript + eval.

”Drop-in” here means: typical Node servers and CLIs that render strings or disk-backed templates—not every feature on the Nunjucks API page (precompile, browser bundles, and some loaders are out of scope today). Async rendering (renderStringAsync, renderTemplateAsync) is supported — see JavaScript API. See Limitations and the repo’s NUNJUCKS_PARITY.md for the full matrix.

  • renderString / render with string templates — same overall pattern; context remains a plain object (JSON-shaped going into Rust).
  • configure, Environment, compile, Template — same ideas; reuse Template instances across renders for steady-state performance.
  • In-memory template graphssetTemplateMap({ name: source, … }) replaces loader-only setups that used a custom loader returning map-backed sources.
  • Filesystem templates — use setLoaderRoot(absolutePath) instead of FileSystemLoader + Environment with a path loader (paths must stay under the root; .. is rejected).
  • Expressrequire('@zneep/runjucks/express').expressEngine(app, opts?) registers app.engine with disk-backed templates; see JavaScript API.
  • Globals and callablesaddGlobal(name, fn) supports JavaScript functions for {{ myFn(…) }} (keyword args follow Nunjucks conventions). Pass callables on the environment, not inside ad-hoc context objects, for predictable bridging.
  1. Install — Remove nunjucks and add @zneep/runjucks (scoped package name; npm rejects the unscoped name runjucks as too close to nunjucks).

  2. Update imports — Point require / import at @zneep/runjucks (and @zneep/runjucks/express for Express).

    // ESM
    import { renderString, Environment, configure } from '@zneep/runjucks'
    // CommonJS
    const { renderString, Environment, configure } = require('@zneep/runjucks')
  3. Replace loaders — If you used new nunjucks.Environment([loader], opts), switch to new Environment() (or configure) plus setTemplateMap or setLoaderRoot / setLoaderCallback as needed. There is no built-in http(s): loader; fetch in JS inside setLoaderCallback if you must resolve remote templates synchronously.

  4. Express — Replace nunjucks.configure + app.engine wiring with expressEngine (express.js entry); merge any configure options into expressEngine(app, { configure: { … } }).

  5. Build — Consumers need a release native addon in production (npm run build from source when developing the library; published installs ship per-platform binaries). Run your template smoke tests after the swap.

Nunjucks ideaRunjucks
nunjucks.renderString(str, ctx)renderString(str, ctx) or env.renderString(str, ctx)
nunjucks.configure(opts)configure(opts) — same pattern for default env
new nunjucks.Environment(loaders?, opts)new Environment() then setTemplateMap / setLoaderRoot / setLoaderCallback
env.renderString, env.renderenv.renderString, env.renderTemplate (named templates)
env.render (async callback)env.renderTemplateAsync(name, ctx)Promise<string>
env.addFilter (async)env.addAsyncFilter(name, fn)
asyncEach / asyncAll / ifAsyncSupported in renderStringAsync / renderTemplateAsync
FileSystemLoader pathsetLoaderRoot(absoluteDir)
Custom sync loadersetLoaderCallback((name) => src or null)
env.addFilter, addGlobal, addExtensionSame names on Environment (details)
compile / TemplateSupported; Template parses once per instance
installJinjaCompat() for slicesNot required for array slices in Runjucks (limitations)

For the full surface (including throwOnUndefined, trimBlocks, custom tags, and extension declarative model), see JavaScript API.

Work through these with your real templates and tests:

  • Async renderingrenderStringAsync and renderTemplateAsync return Promise<string> and support asyncEach, asyncAll, ifAsync, addAsyncFilter, and addAsyncGlobal. JS callbacks currently run synchronously; see JavaScript API.
  • No precompile / browser UMD — Runjucks is a Node native addon. There is no Nunjucks-style JS precompile artifact.
  • Autoescape — Only boolean global autoescape per environment; Nunjucks’ string form (extension-based) is not implemented (limitations).
  • Context shape — Render context crosses the boundary as JSON-compatible data. Use addGlobal for injectable functions; use serialize-context if you relied on Map / Set in context.
  • Custom extensions — Tag bodies are parsed by the engine; your extension supplies a process callback, not Nunjucks’ parse(parser, nodes) hook (limitations).
  • Errors — Stack traces and message shapes differ from Nunjucks; adjust logging and any brittle string assertions.

Golden / parity tests in the repo run nunjucks 3.2.4 against allowlisted fixtures (__test__/parity.test.mjs); treat that as regression signal, not a promise about every edge case.

For experiments against a forked Nunjucks test util.js, the repo provides a minimal sync Environment + Template wrapper in test-shim/nunjucks-compat.js. It does not run upstream’s full Mocha suite; read test-shim/README.md for limits.

  • Template language — syntax reference for tags and expressions.
  • Performance — caching, release builds, and published vs-Nunjucks numbers.
  • Jinja2 background — if your team thinks in Jinja2 terms more than Nunjucks internals.