Skip to content

Provide a way to discover a template's dependencies #1153

Closed
@chocolateboy

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",
    },
]

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions