vite-njk-trace

Dev-only Vite plugin that wraps every Nunjucks template (and rendered page) with HTML comments so you can see template origins in DevTools.


Keywords
vite, vite-plugin, nunjucks, njk, debug, trace, devtools, templates, ssg
License
MIT
Install
npm install vite-njk-trace@1.0.1

Documentation

vite-njk-trace

License: MIT Vite 5–8 Node >= 18 Nunjucks ^3

Dev-only Vite plugin that injects Nunjucks template boundaries into your rendered HTML as comments, so every fragment in DevTools' Elements panel reveals exactly which .njk file it came from.


Features

  • Loader trace — wraps every {% include %}, {% extends %}, and {% import %} template with <!-- NJK: START … --> / <!-- NJK: END … --> comments.
  • Page wrapper — wraps the rendered page body via transformIndexHtml (post order) so the page itself is also annotated.
  • Zero-config dev-only — registered with apply: "serve", so vite build is unaffected and ships no trace comments.
  • Reference counted — multiple enableNjkTraceLoader calls are balanced; the prototype is only patched on the first call and restored on the last.
  • Lazy nunjucks resolutionnunjucks is loaded with createRequire only when the plugin runs. Installing vite-njk-trace in a non-Nunjucks project does not throw ERR_MODULE_NOT_FOUND.
  • Hardened paths — leading separators in pagesDir are stripped, and every label uses forward slashes regardless of host OS.
  • Sanitised HTML--> in template paths is escaped to -- > so injected comments are always well-formed.
  • Dual ESM/CJS — ships dist/index.mjs, dist/index.cjs, dist/index.d.ts, and dist/index.d.cts with full TypeScript declarations.
  • No side effects"sideEffects": false for safe tree-shaking.

Compatibility

Dependency Supported versions
Vite ^5.0.0, ^6.0.0, ^7.0.0, ^8.0.0
Nunjucks ^3.0.0
Node >=18

Both vite and nunjucks are listed as optional peer dependencies — install them in your project if they are not already present.


Installation

pnpm add -D vite-njk-trace
# or
npm install -D vite-njk-trace
# or
yarn add -D vite-njk-trace

Quick start

// vite.config.ts
import { defineConfig } from "vite";
import path from "node:path";
import { fileURLToPath } from "node:url";
import viteNjkTrace from "vite-njk-trace";

const projectRoot = path.dirname(fileURLToPath(import.meta.url));

export default defineConfig({
  plugins: [
    viteNjkTrace({
      projectRoot,
      pagesDir: "frontend/src/views/pages",
    }),
  ],
});

Now open http://localhost:5173/ in your browser, inspect any element, and you will see comments like:

<!-- NJK: START frontend/src/views/partials/_header.njk -->
<header></header>
<!-- NJK: END frontend/src/views/partials/_header.njk -->

<main>
  <h1>Home</h1>
  <!-- NJK: START frontend/src/views/partials/_card.njk -->
  <section class="card"></section>
  <!-- NJK: END frontend/src/views/partials/_card.njk -->
</main>

<!-- NJK: START frontend/src/views/pages/index.njk -->
<!-- NJK: END frontend/src/views/pages/index.njk -->

Order of plugins / Troubleshooting

vite-njk-trace uses enforce: "post" and the configResolved hook so that the Nunjucks loader patch is applied after all normal plugins (including template-engine plugins like vite-plugin-njk-frontmatter) have finished their own setup. This means plugin order in vite.config.js no longer affects correctness.

For best practice, place viteNjkTrace() after your template-engine plugins in the plugins array:

export default defineConfig({
  plugins: [
    vitePluginNjkFrontmatter(),   // template engine plugin
    viteNjkTrace({ projectRoot }), // ← after template plugins (recommended)
  ],
});

If you only see the root template (index.njk) traced but not its included partials, ensure that your template engine plugin registers its Nunjucks Environment / FileSystemLoader during the config or configResolved phase — hooks that run after configResolved (e.g. configureServer) are too late for the loader patch to pick up the loader instance.


Configuration

Option Type Default Description
projectRoot string process.cwd() Absolute root for resolving relative template paths in the HTML comments.
pagesDir string "frontend/src/views/pages" Path to the directory of .njk pages, relative to projectRoot. Leading slashes are stripped.
enablePagesTrace boolean true When true, the page wrapper (via transformIndexHtml) is enabled.

API

viteNjkTrace(options?) (default export)

Returns a PluginOption[] containing:

  1. vite-njk-trace:loader — monkey-patches FileSystemLoader.prototype.getSource (dev only).
  2. vite-njk-trace:pages — wraps the rendered page via transformIndexHtml (only when enablePagesTrace is true).

Both sub-plugins are dev-only (apply: "serve").

Programmatic / standalone

import nunjucks from "nunjucks";
import {
  enableNjkTraceLoader,
  disableNjkTraceLoader,
} from "vite-njk-trace";

enableNjkTraceLoader({ projectRoot: __dirname });
nunjucks.configure("frontend/src/views", { autoescape: false });
console.log(nunjucks.render("pages/index.njk"));
disableNjkTraceLoader();

Sub-plugins

import viteNjkTrace, { njkTracePages } from "vite-njk-trace";

export default defineConfig({
  plugins: [
    viteNjkTrace({ enablePagesTrace: false }),
    njkTracePages({ pagesDir: "src/pages" }),
  ],
});

Exported symbols

Export Kind Description
default / viteNjkTracePlugin function Vite plugin: returns the loader + (optional) pages sub-plugins.
enableNjkTraceLoader function Wraps FileSystemLoader.prototype.getSource. Reference-counted.
disableNjkTraceLoader function Decrements the refcount; restores original getSource only at 0.
isWrapped function Inspect whether the loader prototype is currently patched.
getPatchCount function Returns the current reference count for the loader patch.
njkTracePagesPlugin function Builds the page-wrapper Vite plugin (transformIndexHtml).
njkTracePages function Convenience wrapper that resolves user options and returns a Vite plugin.
START / END function Comment generators (<!-- NJK: START … --> / <!-- NJK: END … -->).

Behaviour

  • Dev-only. apply: "serve" keeps the plugin out of the production bundle.
  • Reference counted. Each enableNjkTraceLoader increments, each disableNjkTraceLoader decrements. The prototype is patched on 0 → 1 and restored on 1 → 0.
  • Safe. If nunjucks.FileSystemLoader cannot be resolved, enableNjkTraceLoader logs a warning and returns a no-op teardown. The plugin never throws at import time.
  • Path safe. Leading separators in pagesDir are stripped so it can never be promoted to an absolute path. Labels are always forward-slash, regardless of host OS.
  • HTML safe. Any --> inside a template path is escaped to -- > so injected comments remain well-formed.
  • Page resolution. The page wrapper tries <base>.njk first, then <base>/index.njk, mirroring Vite's HTML routing.

Project layout

vite-njk-trace/
├── src/
│   ├── index.ts          # Public API + default plugin (PluginOption[])
│   ├── loader.ts         # Lazy FileSystemLoader prototype patch (ref-counted)
│   ├── pages-plugin.ts   # transformIndexHtml wrapper (path-hardened)
│   └── types.ts          # Options interface
├── tests/
│   └── unit/             # Vitest unit tests
├── tests_plugin/         # Manual integration playground
├── dist/                 # Build output (tsup): {index.cjs,index.mjs,index.d.ts,index.d.cts}
├── tsup.config.ts
├── tsconfig.json
├── vitest.config.ts
└── package.json

Scripts

pnpm build            # tsup → dist/{index.cjs,index.mjs,index.d.ts,index.d.cts}
pnpm test             # vitest run
pnpm typecheck        # tsc --noEmit
pnpm prepublishOnly   # build + test

Manual playground

# from the repo root
pnpm install
pnpm --filter vite-njk-trace build
pnpm --filter vite-njk-trace-playground dev
# → http://localhost:5173/

License

MIT © vinyardrip