Limitations and differences
Runjucks targets Node.js and synchronous rendering. This page lists product-level gaps and quirks. For a maintainer-facing checklist, see NUNJUCKS_PARITY.md in the repository (including Testing model: full parity vs partial vs Runjucks-only JSON goldens). For throughput and caching (what is not a limitation), see Performance.
Node.js and loaders
Section titled “Node.js and loaders”autoescapeoption — Nunjucks storesopts.autoescapeand uses it in truthy checks for escaping.configure({ autoescape })accepts boolean, string, number,null, orundefinedand normalizes to a single engine flag:false,0,"",null, andundefined(when explicitly passed) turn escaping off; other values (including non-empty strings like"html") turn it on. The Rust core still uses one boolean per environment — there is no per-filename extension switch like some mozilla.io examples describe.setAutoescaperemains boolean-only.- Filesystem templates — call
setLoaderRoot(absolutePath)on anEnvironmentso named templates load from disk (relative paths under that root;..traversal is rejected). Alternatively usesetTemplateMapwith an object of name → source strings,setLoaderCallbackfor a sync JSgetSource(name)(no built-inhttp(s):loader in native code — see below). - HTTP(S) / URL templates (Node) — Nunjucks’ browser
WebLoaderfetches over HTTP; on Node, Runjucks keepsrender/renderTemplatesynchronous, so load sources outside the engine:await fetch(url)(or your HTTP client), build aRecord<name, source>, thensetTemplateMap(map)orsetLoaderCallback((name) => map[name] ?? null). The package includes@zneep/runjucks/fetch-template-map(fetchTemplateMap(entries)) as a small helper that returns the map forsetTemplateMap. Avoid blocking HTTP insidesetLoaderCallback— prefetch, then render. - Express — optional helper
require('@zneep/runjucks/express').expressEngine(app, opts?)registersapp.enginefor.njk(or your chosenext) usingsetLoaderRootfromapp.get('views')oropts.views. Rendering is synchronous; there is no asyncrendercallback like some Nunjucks setups. - No browser / UMD bundle as a first-class artifact — the runtime is a native addon for Node.
Async and precompile
Section titled “Async and precompile”- Async rendering —
renderStringAsyncandrenderTemplateAsyncreturn aPromise<string>and support async-only tags (asyncEach,asyncAll,ifAsync) as well as async filters (addAsyncFilter) and async globals (addAsyncGlobal). JS callbacks registered via these methods currently run synchronously on the main thread; the Promise-based API matches the Nunjucks surface for forward compatibility. See JavaScript API for usage. - No
precompile/precompileStringemitting JavaScript — the Rust engine parses templates to an internal AST and caches parses per environment /Template(see JavaScript API); there is no Nunjucks-style JS precompile artifact or browser bundle workflow.
Globals and callables
Section titled “Globals and callables”addGlobal(name, value)accepts JSON-serializable values or a JavaScript function for Nunjucks-style{{ fn(…) }}calls (same thread asrender; keyword arguments become a trailing plain object). SeeNUNJUCKS_PARITY.md(P1).- Render context (
renderString(…, ctx)) is still JSON-shaped — you cannot pass live functions insidectxand expect them to be invoked from templates (useaddGlobalon the environment instead).
Custom extensions
Section titled “Custom extensions”addExtensionuses a declarative model: tag names, optional block end names, and aprocesscallback. Nunjucks’ parser hook (parse(parser, nodes)) for custom AST nodes is not exposed.
Import / include / extends (nuances)
Section titled “Import / include / extends (nuances)”import/from: only top-level macros are collected; side effects from running imported templates are not the same as Nunjucks in every edge case. Modifiers likewith contexton imports are parsed but not always equivalent to upstream.include: Runjucks parseswithout contextandwith contexton{% include %}(seenative/crates/runjucks-core/tests/composition.rsand__test__/tags-extended.test.mjsin the repo). Stock nunjucks 3.2.4 does not accept those modifiers oninclude(it will parse-error). JSON conformance cases that must match nunjucks line-for-line (__test__/parity.test.mjs) therefore use plain includes and nested includes; behavior forinclude+ context modifiers is covered by Rust tests and Node tests, not by the npm parity gate.extends: dynamic parent names resolve at render time. A literal-only{% extends "…" %}chain is checked for cycles before render (in addition to runtime resolution); dynamic{% extends expr %}is not analyzed statically.
Filters and types
Section titled “Filters and types”- Some filters differ in edge cases (e.g.
lengthon non-array objects, safe-string chaining vs Nunjucks). Prefer conformance tests or side-by-side checks for critical templates. undefinedvsnullfrom JavaScript both map into the engine’s JSON-style value model — do not rely on distinct runtime behavior between them in templates.
Map, Set, RegExp, and incremental parity
Section titled “Map, Set, RegExp, and incremental parity”Map/Setin render context are not automatically expanded into JSON objects — pass plain objects/arrays or userequire('@zneep/runjucks/serialize-context').serializeContextForRender(obj)for an explicitMap/Set→ JSON conversion at the boundary.- Regular expressions in templates use the Rust engine’s regex support, not full ECMAScript
RegExpsemantics; seeNUNJUCKS_PARITY.mdfor flags and limitations. - Filter safeness (
escape,safe,forceescape) and copy-on-escape behavior: tighten with targeted tests when a real template shows a gap; the repo does not guarantee bit-for-bit Nunjucks output for every edge chain.
Jinja compatibility
Section titled “Jinja compatibility”- Runjucks accepts array slice syntax without requiring a separate
installJinjaCompat()-style shim (Nunjucks needs that for slices). A dedicated Jinja-compat API flag is not required for slices; other Jinja shims from Nunjucks are not mirrored as a single API. @zneep/runjucks/install-jinja-compatexportsinstallJinjaCompat()as a no-op so legacyrequire('…/install-jinja-compat')during migration does not throw; you do not need to call it for slices in Runjucks.