Files
beall-11ty/node_modules/@11ty/eleventy/src/TemplateContent.js

749 lines
20 KiB
JavaScript
Raw Normal View History

2026-03-31 16:38:22 -07:00
import os from "node:os";
import fs from "node:fs";
import matter from "gray-matter";
import lodash from "@11ty/lodash-custom";
import { TemplatePath } from "@11ty/eleventy-utils";
import debugUtil from "debug";
import TemplateData from "./Data/TemplateData.js";
import TemplateRender from "./TemplateRender.js";
import EleventyBaseError from "./Errors/EleventyBaseError.js";
import EleventyErrorUtil from "./Errors/EleventyErrorUtil.js";
import eventBus from "./EventBus.js";
import { withResolvers } from "./Util/PromiseUtil.js";
const { set: lodashSet } = lodash;
const debug = debugUtil("Eleventy:TemplateContent");
const debugDev = debugUtil("Dev:Eleventy:TemplateContent");
class TemplateContentFrontMatterError extends EleventyBaseError {}
class TemplateContentCompileError extends EleventyBaseError {}
class TemplateContentRenderError extends EleventyBaseError {}
class TemplateContent {
#initialized = false;
#config;
#templateRender;
#preprocessorEngine;
#extensionMap;
#configOptions;
constructor(inputPath, templateConfig) {
if (!templateConfig || templateConfig.constructor.name !== "TemplateConfig") {
throw new Error("Missing or invalid `templateConfig` argument");
}
this.eleventyConfig = templateConfig;
this.inputPath = inputPath;
}
async asyncTemplateInitialization() {
if (!this.hasTemplateRender()) {
await this.getTemplateRender();
}
if (this.#initialized) {
return;
}
this.#initialized = true;
let preprocessorEngineName = this.templateRender.getPreprocessorEngineName();
if (preprocessorEngineName && this.templateRender.engine.getName() !== preprocessorEngineName) {
let engine = await this.templateRender.getEngineByName(preprocessorEngineName);
this.#preprocessorEngine = engine;
}
}
resetCachedTemplate({ eleventyConfig }) {
this.eleventyConfig = eleventyConfig;
}
get dirs() {
return this.eleventyConfig.directories;
}
get inputDir() {
return this.dirs.input;
}
get outputDir() {
return this.dirs.output;
}
getResetTypes(types) {
if (types) {
return Object.assign(
{
data: false,
read: false,
render: false,
},
types,
);
}
return {
data: true,
read: true,
render: true,
};
}
// Called during an incremental build when the template instance is cached but needs to be reset because it has changed
resetCaches(types) {
types = this.getResetTypes(types);
if (types.read) {
delete this.readingPromise;
delete this.inputContent;
delete this._frontMatterDataCache;
}
if (types.render) {
this.#templateRender = undefined;
}
}
get extensionMap() {
if (!this.#extensionMap) {
throw new Error("Internal error: Missing `extensionMap` in TemplateContent.");
}
return this.#extensionMap;
}
set extensionMap(map) {
this.#extensionMap = map;
}
set eleventyConfig(config) {
this.#config = config;
if (this.#config.constructor.name === "TemplateConfig") {
this.#configOptions = this.#config.getConfig();
} else {
throw new Error("Tried to get an TemplateConfig but none was found.");
}
}
get eleventyConfig() {
if (this.#config.constructor.name === "TemplateConfig") {
return this.#config;
}
throw new Error("Tried to get an TemplateConfig but none was found.");
}
get config() {
if (this.#config.constructor.name === "TemplateConfig" && !this.#configOptions) {
this.#configOptions = this.#config.getConfig();
}
return this.#configOptions;
}
get bench() {
return this.config.benchmarkManager.get("Aggregate");
}
get engine() {
return this.templateRender.engine;
}
get templateRender() {
if (!this.hasTemplateRender()) {
throw new Error(`\`templateRender\` has not yet initialized on ${this.inputPath}`);
}
return this.#templateRender;
}
hasTemplateRender() {
return !!this.#templateRender;
}
async getTemplateRender() {
if (!this.#templateRender) {
this.#templateRender = new TemplateRender(this.inputPath, this.eleventyConfig);
this.#templateRender.extensionMap = this.extensionMap;
return this.#templateRender.init().then(() => {
return this.#templateRender;
});
}
return this.#templateRender;
}
// For monkey patchers
get frontMatter() {
if (this.frontMatterOverride) {
return this.frontMatterOverride;
} else {
throw new Error(
"Unfortunately youre using code that monkey patched some Eleventy internals and it isnt async-friendly. Change your code to use the async `read()` method on the template instead!",
);
}
}
// For monkey patchers
set frontMatter(contentOverride) {
this.frontMatterOverride = contentOverride;
}
getInputPath() {
return this.inputPath;
}
getInputDir() {
return this.inputDir;
}
isVirtualTemplate() {
let def = this.getVirtualTemplateDefinition();
return !!def;
}
getVirtualTemplateDefinition() {
let inputDirRelativeInputPath =
this.eleventyConfig.directories.getInputPathRelativeToInputDirectory(this.inputPath);
return this.config.virtualTemplates[inputDirRelativeInputPath];
}
async #read() {
let content = await this.inputContent;
if (content || content === "") {
let tr = await this.getTemplateRender();
if (tr.engine.useJavaScriptImport()) {
return {
data: {},
content,
};
}
let options = this.config.frontMatterParsingOptions || {};
let fm;
try {
// Added in 3.0, passed along to front matter engines
options.filePath = this.inputPath;
fm = matter(content, options);
} catch (e) {
throw new TemplateContentFrontMatterError(
`Having trouble reading front matter from template ${this.inputPath}`,
e,
);
}
if (options.excerpt && fm.excerpt) {
let excerptString = fm.excerpt + (options.excerpt_separator || "---");
if (fm.content.startsWith(excerptString + os.EOL)) {
// with an os-specific newline after excerpt separator
fm.content = fm.excerpt.trim() + "\n" + fm.content.slice((excerptString + os.EOL).length);
} else if (fm.content.startsWith(excerptString + "\n")) {
// with a newline (\n) after excerpt separator
// This is necessary for some git configurations on windows
fm.content = fm.excerpt.trim() + "\n" + fm.content.slice((excerptString + 1).length);
} else if (fm.content.startsWith(excerptString)) {
// no newline after excerpt separator
fm.content = fm.excerpt + fm.content.slice(excerptString.length);
}
// alias, defaults to page.excerpt
let alias = options.excerpt_alias || "page.excerpt";
lodashSet(fm.data, alias, fm.excerpt);
}
// For monkey patchers that used `frontMatter` 🤧
// https://github.com/11ty/eleventy/issues/613#issuecomment-999637109
// https://github.com/11ty/eleventy/issues/2710#issuecomment-1373854834
// Removed this._frontMatter monkey patcher help in 3.0.0-alpha.7
return fm;
} else {
return {
data: {},
content: "",
excerpt: "",
};
}
}
async read() {
if (!this.readingPromise) {
if (!this.inputContent) {
// @cachedproperty
this.inputContent = this.getInputContent();
}
// @cachedproperty
this.readingPromise = this.#read();
}
return this.readingPromise;
}
/* Incremental builds cache the Template instances (in TemplateWriter) but
* these template specific caches are important for Pagination */
static cache(path, content) {
this._inputCache.set(TemplatePath.absolutePath(path), content);
}
static getCached(path) {
return this._inputCache.get(TemplatePath.absolutePath(path));
}
static deleteFromInputCache(path) {
this._inputCache.delete(TemplatePath.absolutePath(path));
}
// Used via clone
setInputContent(content) {
this.inputContent = content;
}
async getInputContent() {
let tr = await this.getTemplateRender();
let virtualTemplateDefinition = this.getVirtualTemplateDefinition();
if (virtualTemplateDefinition) {
let { content } = virtualTemplateDefinition;
return content;
}
if (
tr.engine.useJavaScriptImport() &&
typeof tr.engine.getInstanceFromInputPath === "function"
) {
return tr.engine.getInstanceFromInputPath(this.inputPath);
}
if (!tr.engine.needsToReadFileContents()) {
return "";
}
let templateBenchmark = this.bench.get("Template Read");
templateBenchmark.before();
let content;
if (this.config.useTemplateCache) {
content = TemplateContent.getCached(this.inputPath);
}
if (!content && content !== "") {
let contentBuffer = fs.readFileSync(this.inputPath);
content = contentBuffer.toString("utf8");
if (this.config.useTemplateCache) {
TemplateContent.cache(this.inputPath, content);
}
}
templateBenchmark.after();
return content;
}
async _testGetFrontMatter() {
let fm = this.frontMatterOverride ? this.frontMatterOverride : await this.read();
return fm;
}
async getPreRender() {
let fm = this.frontMatterOverride ? this.frontMatterOverride : await this.read();
return fm.content;
}
async #getFrontMatterData() {
let fm = await this.read();
// gray-matter isnt async-friendly but can return a promise from custom front matter
if (fm.data instanceof Promise) {
fm.data = await fm.data;
}
let tr = await this.getTemplateRender();
let extraData = await tr.engine.getExtraDataFromFile(this.inputPath);
let virtualTemplateDefinition = this.getVirtualTemplateDefinition();
let virtualTemplateData;
if (virtualTemplateDefinition) {
virtualTemplateData = virtualTemplateDefinition.data;
}
let data = Object.assign(fm.data, extraData, virtualTemplateData);
TemplateData.cleanupData(data, {
file: this.inputPath,
isVirtualTemplate: Boolean(virtualTemplateData),
});
return {
data,
excerpt: fm.excerpt,
};
}
async getFrontMatterData() {
if (!this._frontMatterDataCache) {
// @cachedproperty
this._frontMatterDataCache = this.#getFrontMatterData();
}
return this._frontMatterDataCache;
}
async getEngineOverride() {
return this.getFrontMatterData().then((data) => {
return data[this.config.keys.engineOverride];
});
}
// checks engines
isTemplateCacheable() {
if (this.#preprocessorEngine) {
return this.#preprocessorEngine.cacheable;
}
return this.engine.cacheable;
}
_getCompileCache(str) {
// Caches used to be bifurcated based on engine name, now theyre based on inputPath
// TODO does `cacheable` need to help inform whether a cache is used here?
let inputPathMap = TemplateContent._compileCache.get(this.inputPath);
if (!inputPathMap) {
inputPathMap = new Map();
TemplateContent._compileCache.set(this.inputPath, inputPathMap);
}
let cacheable = this.isTemplateCacheable();
let { useCache, key } = this.engine.getCompileCacheKey(str, this.inputPath);
// We also tie the compile cache key to the UserConfig instance, to alleviate issues with global template cache
// Better to move the cache to the Eleventy instance instead, no?
// (This specifically failed I18nPluginTest cases with filters being cached across tests and not having access to each plugins options)
key = this.eleventyConfig.userConfig._getUniqueId() + key;
return [cacheable, key, inputPathMap, useCache];
}
async compile(str, options = {}) {
let { type, bypassMarkdown, engineOverride } = options;
// Must happen before cacheable fetch below
// Likely only necessary for Eleventy Layouts, see TemplateMap->initDependencyMap
await this.asyncTemplateInitialization();
// this.templateRender is guaranteed here
let tr = await this.getTemplateRender();
if (engineOverride !== undefined) {
debugDev("%o overriding template engine to use %o", this.inputPath, engineOverride);
await tr.setEngineOverride(engineOverride, bypassMarkdown);
} else {
tr.setUseMarkdown(!bypassMarkdown);
}
if (bypassMarkdown && !this.engine.needsCompilation(str)) {
return function () {
return str;
};
}
debugDev("%o compile() using engine: %o", this.inputPath, tr.engineName);
try {
let res;
if (this.config.useTemplateCache) {
let [cacheable, key, cache, useCache] = this._getCompileCache(str);
if (cacheable && key) {
if (useCache && cache.has(key)) {
this.bench.get("(count) Template Compile Cache Hit").incrementCount();
return cache.get(key);
}
this.bench.get("(count) Template Compile Cache Miss").incrementCount();
// Compile cache is cleared when the resource is modified (below)
// Compilation is async, so we eagerly cache a Promise that eventually
// resolves to the compiled function
let withRes = withResolvers();
res = withRes.resolve;
cache.set(key, withRes.promise);
}
}
let typeStr = type ? ` ${type}` : "";
let templateBenchmark = this.bench.get(`Template Compile${typeStr}`);
let inputPathBenchmark = this.bench.get(`> Compile${typeStr} > ${this.inputPath}`);
templateBenchmark.before();
inputPathBenchmark.before();
let fn = await tr.getCompiledTemplate(str);
inputPathBenchmark.after();
templateBenchmark.after();
debugDev("%o getCompiledTemplate function created", this.inputPath);
if (this.config.useTemplateCache && res) {
res(fn);
}
return fn;
} catch (e) {
let [cacheable, key, cache] = this._getCompileCache(str);
if (cacheable && key) {
cache.delete(key);
}
debug(`Having trouble compiling template ${this.inputPath}: %O`, str);
throw new TemplateContentCompileError(
`Having trouble compiling template ${this.inputPath}`,
e,
);
}
}
getParseForSymbolsFunction(str) {
let engine = this.engine;
// Dont use markdown as the engine to parse for symbols
// TODO pass in engineOverride here
if (this.#preprocessorEngine) {
engine = this.#preprocessorEngine;
}
if ("parseForSymbols" in engine) {
return () => {
if (Array.isArray(str)) {
return str
.filter((entry) => typeof entry === "string")
.map((entry) => engine.parseForSymbols(entry))
.flat();
}
if (typeof str === "string") {
return engine.parseForSymbols(str);
}
return [];
};
}
}
// used by computed data or for permalink functions
async _renderFunction(fn, ...args) {
let mixins = Object.assign({}, this.config.javascriptFunctions);
let result = await fn.call(mixins, ...args);
// normalize Buffer away if returned from permalink
if (Buffer.isBuffer(result)) {
return result.toString();
}
return result;
}
async renderComputedData(str, data) {
if (typeof str === "function") {
return this._renderFunction(str, data);
}
return this._render(str, data, {
type: "Computed Data",
bypassMarkdown: true,
});
}
async renderPermalink(permalink, data) {
let tr = await this.getTemplateRender();
let permalinkCompilation = tr.engine.permalinkNeedsCompilation(permalink);
// No string compilation:
// ({ compileOptions: { permalink: "raw" }})
// These mean `permalink: false`, which is no file system writing:
// ({ compileOptions: { permalink: false }})
// ({ compileOptions: { permalink: () => false }})
// ({ compileOptions: { permalink: () => (() = > false) }})
if (permalinkCompilation === false && typeof permalink !== "function") {
return permalink;
}
/* Custom `compile` function for permalinks, usage:
permalink: function(permalinkString, inputPath) {
return async function(data) {
return "THIS IS MY RENDERED PERMALINK";
}
}
*/
if (permalinkCompilation && typeof permalinkCompilation === "function") {
permalink = await this._renderFunction(permalinkCompilation, permalink, this.inputPath);
}
// Raw permalink function (in the app code data cascade)
if (typeof permalink === "function") {
return this._renderFunction(permalink, data);
}
return this._render(permalink, data, {
type: "Permalink",
bypassMarkdown: true,
});
}
async render(str, data, bypassMarkdown) {
return this._render(str, data, {
type: "Content",
bypassMarkdown,
});
}
_getPaginationLogSuffix(data) {
let suffix = [];
if ("pagination" in data) {
suffix.push(" (");
if (data.pagination.pages) {
suffix.push(
`${data.pagination.pages.length} page${data.pagination.pages.length !== 1 ? "s" : ""}`,
);
} else {
suffix.push("Pagination");
}
suffix.push(")");
}
return suffix.join("");
}
async _render(str, data, options = {}) {
let { bypassMarkdown, type } = options;
try {
if (bypassMarkdown && !this.engine.needsCompilation(str)) {
return str;
}
let fn = await this.compile(str, {
bypassMarkdown,
engineOverride: data[this.config.keys.engineOverride],
type,
});
if (fn === undefined) {
return;
} else if (typeof fn !== "function") {
throw new Error(`The \`compile\` function did not return a function. Received ${fn}`);
}
// Benchmark
let templateBenchmark = this.bench.get("Render");
let inputPathBenchmark = this.bench.get(
`> Render${type ? ` ${type}` : ""} > ${this.inputPath}${this._getPaginationLogSuffix(data)}`,
);
templateBenchmark.before();
if (inputPathBenchmark) {
inputPathBenchmark.before();
}
let rendered = await fn(data);
if (inputPathBenchmark) {
inputPathBenchmark.after();
}
templateBenchmark.after();
debugDev("%o getCompiledTemplate called, rendered content created", this.inputPath);
return rendered;
} catch (e) {
if (EleventyErrorUtil.isPrematureTemplateContentError(e)) {
return Promise.reject(e);
} else {
let tr = await this.getTemplateRender();
let engine = tr.getReadableEnginesList();
debug(`Having trouble rendering ${engine} template ${this.inputPath}: %O`, str);
return Promise.reject(
new TemplateContentRenderError(
`Having trouble rendering ${engine} template ${this.inputPath}`,
e,
),
);
}
}
}
getExtensionEntries() {
return this.engine.extensionEntries;
}
isFileRelevantToThisTemplate(incrementalFile, metadata = {}) {
// always relevant if incremental file not set (build everything)
if (!incrementalFile) {
return true;
}
let hasDependencies = this.engine.hasDependencies(incrementalFile);
let isRelevant = this.engine.isFileRelevantTo(this.inputPath, incrementalFile);
debug(
"Test dependencies to see if %o is relevant to %o: %o",
this.inputPath,
incrementalFile,
isRelevant,
);
let extensionEntries = this.getExtensionEntries().filter((entry) => !!entry.isIncrementalMatch);
if (extensionEntries.length) {
for (let entry of extensionEntries) {
if (
entry.isIncrementalMatch.call(
{
inputPath: this.inputPath,
isFullTemplate: metadata.isFullTemplate,
isFileRelevantToInputPath: isRelevant,
doesFileHaveDependencies: hasDependencies,
},
incrementalFile,
)
) {
return true;
}
}
return false;
} else {
// Not great way of building all templates if this is a layout, include, JS dependency.
// TODO improve this for default template syntaxes
// This is the fallback way of determining if something is incremental (no isIncrementalMatch available)
// This will be true if the inputPath and incrementalFile are the same
if (isRelevant) {
return true;
}
// only return true here if dependencies are not known
if (!hasDependencies && !metadata.isFullTemplate) {
return true;
}
}
return false;
}
}
TemplateContent._inputCache = new Map();
TemplateContent._compileCache = new Map();
eventBus.on("eleventy.resourceModified", (path) => {
// delete from input cache
TemplateContent.deleteFromInputCache(path);
// delete from compile cache
let normalized = TemplatePath.addLeadingDotSlash(path);
let compileCache = TemplateContent._compileCache.get(normalized);
if (compileCache) {
compileCache.clear();
}
});
// Used when the configuration file reset https://github.com/11ty/eleventy/issues/2147
eventBus.on("eleventy.compileCacheReset", () => {
TemplateContent._compileCache = new Map();
});
export default TemplateContent;