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.
When migration is usually straightforward
Section titled “When migration is usually straightforward”renderString/renderwith string templates — same overall pattern; context remains a plain object (JSON-shaped going into Rust).configure,Environment,compile,Template— same ideas; reuseTemplateinstances across renders for steady-state performance.- In-memory template graphs —
setTemplateMap({ name: source, … })replaces loader-only setups that used a custom loader returning map-backed sources. - Filesystem templates — use
setLoaderRoot(absolutePath)instead ofFileSystemLoader+Environmentwith a path loader (paths must stay under the root;..is rejected). - Express —
require('@zneep/runjucks/express').expressEngine(app, opts?)registersapp.enginewith disk-backed templates; see JavaScript API. - Globals and callables —
addGlobal(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.
Step-by-step
Section titled “Step-by-step”-
Install — Remove
nunjucksand add@zneep/runjucks(scoped package name; npm rejects the unscoped namerunjucksas too close tonunjucks). -
Update imports — Point
require/importat@zneep/runjucks(and@zneep/runjucks/expressfor Express).// ESMimport { renderString, Environment, configure } from '@zneep/runjucks'// CommonJSconst { renderString, Environment, configure } = require('@zneep/runjucks') -
Replace loaders — If you used
new nunjucks.Environment([loader], opts), switch tonew Environment()(orconfigure) plussetTemplateMaporsetLoaderRoot/setLoaderCallbackas needed. There is no built-inhttp(s):loader; fetch in JS insidesetLoaderCallbackif you must resolve remote templates synchronously. -
Express — Replace
nunjucks.configure+app.enginewiring withexpressEngine(express.jsentry); merge anyconfigureoptions intoexpressEngine(app, { configure: { … } }). -
Build — Consumers need a release native addon in production (
npm run buildfrom source when developing the library; published installs ship per-platform binaries). Run your template smoke tests after the swap.
API mapping (Nunjucks → Runjucks)
Section titled “API mapping (Nunjucks → Runjucks)”| Nunjucks idea | Runjucks |
|---|---|
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.render | env.renderString, env.renderTemplate (named templates) |
env.render (async callback) | env.renderTemplateAsync(name, ctx) → Promise<string> |
env.addFilter (async) | env.addAsyncFilter(name, fn) |
asyncEach / asyncAll / ifAsync | Supported in renderStringAsync / renderTemplateAsync |
FileSystemLoader path | setLoaderRoot(absoluteDir) |
| Custom sync loader | setLoaderCallback((name) => src or null) |
env.addFilter, addGlobal, addExtension | Same names on Environment (details) |
compile / Template | Supported; Template parses once per instance |
installJinjaCompat() for slices | Not required for array slices in Runjucks (limitations) |
For the full surface (including throwOnUndefined, trimBlocks, custom tags, and extension declarative model), see JavaScript API.
Pre-flight checklist (before production)
Section titled “Pre-flight checklist (before production)”Work through these with your real templates and tests:
- Async rendering —
renderStringAsyncandrenderTemplateAsyncreturnPromise<string>and supportasyncEach,asyncAll,ifAsync,addAsyncFilter, andaddAsyncGlobal. 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
addGlobalfor injectable functions; useserialize-contextif you relied onMap/Setin context. - Custom extensions — Tag bodies are parsed by the engine; your extension supplies a
processcallback, 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.
Optional: string-only test shim
Section titled “Optional: string-only test shim”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.
Next steps
Section titled “Next steps”- 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.