Description
nunjucks doesn't provide a way to discover a template's direct or transitive dependencies i.e. the child and grandchild templates that are loaded (via extends
, import
or include
) when a parent template is rendered. This is needed to integrate (efficiently) with bundlers such as Parcel, which track changes in dependencies and use them to trigger rebuilds.
(Note: this is orthogonal to nunjucks' caching support via chokidar, though the implementations may overlap.)
Ideally, this would be scoped to a particular Environment#render
call, but there doesn't appear to be any space in the render
method's parameters to squeeze in additional options, or to return an additional value. Maybe a sister method with a more flexible interface?
// result is an object rather than a string
const result = env.parse(templatePath, { context: ..., trackDependencies: true })
console.log('dependencies:', result.dependencies)
console.log('source:', result.source)
This makes dependency tracking optional, to err on the side of efficiency, but they could be included by default as it's a new API, and the overhead is unlikely to be high e.g.:
const result = env.parse(templatePath, { context: ... })
Another way to do it is via an EventEmitter API (which is already used, lightly, in loaders) e.g.:
env.on('dependency', ({ name, path, parent }) => { ... })
This has several advantages:
- it doesn't clutter or crowd any existing APIs
- it's easier to implement
- it's easy to extend
- if there's a performance concern, it could be toggled on/off with a constructor option e.g.
new Environment(loaders, { trackDependencies: true })
The main disadvantage is that it doesn't directly answer the question "What are this template's dependencies?" i.e. it requires extra work to scope the results to a particular template/render. (This also implies the use of a full EventEmitter implementation rather than the cut-down version that's currently used for loaders, and suggests the addition of some new events e.g. render:start
and render:end
.)
I'm envisaging each dependency as an object with the following core fields:
{
name: "../macros/util.html.njk",
path: "/home/foo/dev/example/src/html/macros/util.html.njk",
parent: "/home/foo/dev/example/src/html/screens/layout.html.njk",
}
Optional fields could include cached
(a boolean indicating that the template was retrieved from its loader's cache) and maybe async
(the template was retrieved by an async loader).
Example
layout.html
{% include "../components/header.html" %}
<h1>Body</h1>
{% include "../components/footer.html" %}
header.html
<h1>Header</h1>
footer.html
<h1>Footer</h1>
{% include "./copyright.html" %}
copyright.html
Copyright ⓒ example.com 2018
render
const result = env.parse("src/html/screens/layout.html")
console.log(result.dependencies)
result
[
{
name: "src/html/screens/layout.html",
path: "/home/foo/example/src/html/screens/layout.html",
parent: null,
},
{
name: "../components/header.html",
path: "/home/foo/example/src/html/components/header.html",
parent: "/home/foo/example/src/html/screens/layout.html",
},
{
name: "../components/footer.html",
path: "/home/foo/example/src/html/components/footer.html",
parent: "/home/foo/example/src/html/screens/layout.html",
},
{
name: "./copyright.html",
path: "/home/foo/example/src/html/components/copyright.html",
parent: "/home/foo/example/src/html/components/footer.html",
},
]