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
.njkfile it came from.
-
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", sovite buildis unaffected and ships no trace comments. -
Reference counted — multiple
enableNjkTraceLoadercalls are balanced; the prototype is only patched on the first call and restored on the last. -
Lazy nunjucks resolution —
nunjucksis loaded withcreateRequireonly when the plugin runs. Installingvite-njk-tracein a non-Nunjucks project does not throwERR_MODULE_NOT_FOUND. -
Hardened paths — leading separators in
pagesDirare 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, anddist/index.d.ctswith full TypeScript declarations. -
No side effects —
"sideEffects": falsefor safe tree-shaking.
| 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.
pnpm add -D vite-njk-trace
# or
npm install -D vite-njk-trace
# or
yarn add -D vite-njk-trace// 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 -->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.
| 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. |
Returns a PluginOption[] containing:
-
vite-njk-trace:loader— monkey-patchesFileSystemLoader.prototype.getSource(dev only). -
vite-njk-trace:pages— wraps the rendered page viatransformIndexHtml(only whenenablePagesTraceistrue).
Both sub-plugins are dev-only (apply: "serve").
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();import viteNjkTrace, { njkTracePages } from "vite-njk-trace";
export default defineConfig({
plugins: [
viteNjkTrace({ enablePagesTrace: false }),
njkTracePages({ pagesDir: "src/pages" }),
],
});| 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 … -->). |
-
Dev-only.
apply: "serve"keeps the plugin out of the production bundle. -
Reference counted. Each
enableNjkTraceLoaderincrements, eachdisableNjkTraceLoaderdecrements. The prototype is patched on0 → 1and restored on1 → 0. -
Safe. If
nunjucks.FileSystemLoadercannot be resolved,enableNjkTraceLoaderlogs a warning and returns a no-op teardown. The plugin never throws at import time. -
Path safe. Leading separators in
pagesDirare 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>.njkfirst, then<base>/index.njk, mirroring Vite's HTML routing.
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
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# from the repo root
pnpm install
pnpm --filter vite-njk-trace build
pnpm --filter vite-njk-trace-playground dev
# → http://localhost:5173/