Files
beall-11ty/node_modules/@11ty/eleventy/src/TemplateContent.js
2026-03-31 16:38:22 -07:00

749 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;