Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions src/Plugins/RenderPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import Liquid from "../Adapters/Engines/Liquid.js";

class EleventyNunjucksError extends EleventyBaseError {}

const compileCache = new WeakMap();
const compileFileCache = new WeakMap();

/** @this {object} */
async function compile(content, templateLang, options = {}) {
let { templateConfig, extensionMap } = options;
Expand All @@ -31,6 +34,15 @@ async function compile(content, templateLang, options = {}) {
templateLang = this.page.templateSyntax;
}

if (!compileCache.has(templateConfig)) {
compileCache.set(templateConfig, new Map());
}
let projectCache = compileCache.get(templateConfig);
let cacheKey = `${content}###${templateLang || ""}`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't touch the assignments, you can use const over let, generally speaking.

if (projectCache.has(cacheKey)) {
return projectCache.get(cacheKey);
}

if (!extensionMap) {
if (strictMode) {
throw new Error("Internal error: missing `extensionMap` in RenderPlugin->compile.");
Expand Down Expand Up @@ -62,7 +74,9 @@ async function compile(content, templateLang, options = {}) {
);
}

return tr.getCompiledTemplate(content);
let fn = await tr.getCompiledTemplate(content);
projectCache.set(cacheKey, fn);
return fn;
}

// No templateLang default, it should infer from the inputPath.
Expand Down Expand Up @@ -94,6 +108,12 @@ async function compileFile(inputPath, options = {}, templateLang) {
);
}

if (!compileFileCache.has(templateConfig)) {
compileFileCache.set(templateConfig, new Map());
}
let projectCache = compileFileCache.get(templateConfig);
let cacheKey = `${normalizedPath}###${templateLang || ""}`;

if (!extensionMap) {
if (strictMode) {
throw new Error("Internal error: missing `extensionMap` in RenderPlugin->compileFile.");
Expand All @@ -112,12 +132,27 @@ async function compileFile(inputPath, options = {}, templateLang) {
}

if (!tr.engine.needsToReadFileContents()) {
return tr.getCompiledTemplate(null);
let fn = await tr.getCompiledTemplate(null);
// Note: we don't cache these yet, but we could.
return fn;
}

// TODO we could make this work with full templates (with front matter?)
let content = readFileSync(inputPath, "utf8");
return tr.getCompiledTemplate(content);

if (projectCache.has(cacheKey)) {
let cached = projectCache.get(cacheKey);
if (cached.content === content) {
return cached.fn;
}
}

let fn = await tr.getCompiledTemplate(content);
projectCache.set(cacheKey, {
content,
fn,
});
return fn;
}

/** @this {object} */
Expand Down
56 changes: 56 additions & 0 deletions test/TemplateRenderPluginTest.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import test from "ava";
import fs from "node:fs";
Copy link
Copy Markdown
Contributor

@Ryuno-Ki Ryuno-Ki Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could pull the functions directly during the import:

Suggested change
import fs from "node:fs";
import { existsSync, unlinkSync, writeFileSync } from "node:fs";


import {
default as RenderPlugin,
Expand All @@ -7,9 +8,12 @@ import {
RenderManager,
} from "../src/Plugins/RenderPlugin.js";
import Eleventy from "../src/Eleventy.js";
import TemplateConfig from "../src/TemplateConfig.js";
import ProjectDirectories from "../src/Util/ProjectDirectories.js";

import { normalizeNewLines } from "./Util/normalizeNewLines.js";


async function getTestOutput(input, configCallback = function () {}) {
let elev = new Eleventy(input, "./_site/", {
config: function (eleventyConfig) {
Expand Down Expand Up @@ -321,3 +325,55 @@ test("#3368 #3810 config init bug with RenderManager", async (t) => {
let results = await elev.toJSON();
t.is(results[0].content, `<h1>Sign up for our newsletter!</h1>`);
});

async function getSharedConfig() {
let templateConfig = new TemplateConfig(null, false);
templateConfig.setDirectories(new ProjectDirectories());
await templateConfig.init();
return templateConfig;
}

test("Verify compileFile returns the same function with memoization (shared config)", async (t) => {
const filePath = "./test/stubs-render-plugin/11tyjs-file.njk";
let templateConfig = await getSharedConfig();
let options = { templateConfig };

let fn1 = await RenderPluginFile(filePath, options);
let fn2 = await RenderPluginFile(filePath, options);

t.is(fn1, fn2);
});

test("Verify compileFile returns different functions for different content (shared config)", async (t) => {
const filePath = "./test/stubs-render-plugin/temp-file.njk";
fs.writeFileSync(filePath, "content 1");

let templateConfig = await getSharedConfig();
let options = { templateConfig };

let fn1 = await RenderPluginFile(filePath, options);

fs.writeFileSync(filePath, "content 2");
let fn2 = await RenderPluginFile(filePath, options);

t.not(fn1, fn2);

if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
});

test("Verify compile (string) returns the same function with memoization (shared config)", async (t) => {
let templateConfig = await getSharedConfig();
let options = { templateConfig };
const content = "Hello {{ name }}";

// Need to mock this.page.templateSyntax if templateLang is not passed
const context = { page: { templateSyntax: "njk" } };

let fn1 = await RenderPluginString.call(context, content, "njk", options);
let fn2 = await RenderPluginString.call(context, content, "njk", options);

t.is(fn1, fn2);
});