epiphany/node_modules/@11ty/eleventy/src/Engines/Custom.js
2023-12-09 22:19:03 -08:00

299 lines
7.9 KiB
JavaScript

const TemplateEngine = require("./TemplateEngine");
const getJavaScriptData = require("../Util/GetJavaScriptData");
const eventBus = require("../EventBus.js");
let lastModifiedFile = undefined;
eventBus.on("eleventy.resourceModified", (path) => {
lastModifiedFile = path;
});
class CustomEngine extends TemplateEngine {
constructor(name, dirs, config) {
super(name, dirs, config);
this.entry = this.getExtensionMapEntry();
this.needsInit =
"init" in this.entry && typeof this.entry.init === "function";
this._defaultEngine = undefined;
// Enable cacheability for this template
if (this.entry.compileOptions && "cache" in this.entry.compileOptions) {
this.cacheable = this.entry.compileOptions.cache;
} else if (this.needsToReadFileContents()) {
this.cacheable = true;
}
}
getExtensionMapEntry() {
if ("extensionMap" in this.config) {
// Iterates over only the user config `addExtension` entries
for (let entry of this.config.extensionMap) {
if (entry.key.toLowerCase() === this.name.toLowerCase()) {
return entry;
}
}
}
throw Error(
`Could not find a custom extension for ${this.name}. Did you add it to your config file?`
);
}
setDefaultEngine(defaultEngine) {
this._defaultEngine = defaultEngine;
}
/**
* @override
*/
needsToReadFileContents() {
if ("read" in this.entry) {
return this.entry.read;
}
// Handle aliases to `11ty.js` templates, avoid reading files in the alias, see #2279
// Here, we are short circuiting fallback to defaultRenderer, does not account for compile
// functions that call defaultRenderer explicitly
if (
this._defaultEngine &&
"needsToReadFileContents" in this._defaultEngine
) {
return this._defaultEngine.needsToReadFileContents();
}
return true;
}
// If we init from multiple places, wait for the first init to finish before continuing on.
async _runningInit() {
if (this.needsInit) {
if (!this._initing) {
this._initBench = this.benchmarks.aggregate.get(
`Engine (${this.name}) Init`
);
this._initBench.before();
this._initing = this.entry.init.bind({
config: this.config,
bench: this.benchmarks.aggregate,
})();
}
await this._initing;
this.needsInit = false;
if (this._initBench) {
this._initBench.after();
this._initBench = undefined;
}
}
}
async getExtraDataFromFile(inputPath) {
if (this.entry.getData === false) {
return;
}
if (!("getData" in this.entry)) {
// Handle aliases to `11ty.js` templates, use upstream default engine data fetch, see #2279
if (
this._defaultEngine &&
"getExtraDataFromFile" in this._defaultEngine
) {
return this._defaultEngine.getExtraDataFromFile(inputPath);
}
return;
}
await this._runningInit();
if (typeof this.entry.getData === "function") {
let dataBench = this.benchmarks.aggregate.get(
`Engine (${this.name}) Get Data From File (Function)`
);
dataBench.before();
let data = this.entry.getData(inputPath);
dataBench.after();
return data;
}
// if getData is not false or a function then `getInstanceFromInputPath` must exist
if (!("getInstanceFromInputPath" in this.entry)) {
return Promise.reject(
new Error(
`getInstanceFromInputPath callback missing from ${this.name} template engine plugin.`
)
);
}
let keys = new Set();
if (this.entry.getData === true) {
keys.add("data");
} else if (Array.isArray(this.entry.getData)) {
for (let key of this.entry.getData) {
keys.add(key);
}
}
let dataBench = this.benchmarks.aggregate.get(
`Engine (${this.name}) Get Data From File`
);
dataBench.before();
let inst = await this.entry.getInstanceFromInputPath(inputPath);
// override keys set at the plugin level in the individual template
if (inst.eleventyDataKey) {
keys = new Set(inst.eleventyDataKey);
}
let mixins;
if (this.config) {
// Object.assign usage: see TemplateRenderCustomTest.js: `JavaScript functions should not be mutable but not *that* mutable`
mixins = Object.assign({}, this.config.javascriptFunctions);
}
let promises = [];
for (let key of keys) {
promises.push(
getJavaScriptData(inst, inputPath, key, {
mixins,
isObjectRequired: key === "data",
})
);
}
let results = await Promise.all(promises);
let data = {};
for (let result of results) {
Object.assign(data, result);
}
dataBench.after();
return data;
}
async compile(str, inputPath, ...args) {
await this._runningInit();
let defaultRenderer;
if (this._defaultEngine) {
defaultRenderer = async (data) => {
const render = await this._defaultEngine.compile(
str,
inputPath,
...args
);
return render(data);
};
}
// Fall back to default compiler if the user does not provide their own
if (!this.entry.compile && defaultRenderer) {
return defaultRenderer;
}
// TODO generalize this (look at JavaScript.js)
let fn = this.entry.compile.bind({
config: this.config,
addDependencies: (from, toArray = []) => {
this.config.uses.addDependency(from, toArray);
},
defaultRenderer, // bind defaultRenderer to compile function
})(str, inputPath);
// Support `undefined` to skip compile/render
if (fn) {
// Bind defaultRenderer to render function
if ("then" in fn && typeof fn.then === "function") {
// Promise, wait to bind
return fn.then((fn) => {
if (typeof fn === "function") {
return fn.bind({ defaultRenderer });
}
return fn;
});
} else if ("bind" in fn && typeof fn.bind === "function") {
return fn.bind({ defaultRenderer });
}
}
return fn;
}
get defaultTemplateFileExtension() {
return this.entry.outputFileExtension;
}
hasDependencies(inputPath) {
if (this.config.uses.getDependencies(inputPath) === false) {
return false;
}
return true;
}
isFileRelevantTo(inputPath, comparisonFile, includeLayouts) {
return this.config.uses.isFileRelevantTo(
inputPath,
comparisonFile,
includeLayouts
);
}
getCompileCacheKey(str, inputPath) {
// Return this separately so we know whether or not to use the cached version
// but still return a key to cache this new render for next time
let useCache = !this.isFileRelevantTo(inputPath, lastModifiedFile, false);
if (
this.entry.compileOptions &&
"getCacheKey" in this.entry.compileOptions
) {
if (typeof this.entry.compileOptions.getCacheKey !== "function") {
throw new Error(
`\`compileOptions.getCacheKey\` must be a function in addExtension for the ${this.name} type`
);
}
return {
useCache,
key: this.entry.compileOptions.getCacheKey(str, inputPath),
};
}
let { key } = super.getCompileCacheKey(str, inputPath);
return {
useCache,
key,
};
}
permalinkNeedsCompilation(str) {
if (this.entry.compileOptions && "permalink" in this.entry.compileOptions) {
let p = this.entry.compileOptions.permalink;
if (p === "raw") {
return false;
}
// permalink: false is aliased to permalink: () => false
if (p === false) {
return () => false;
}
return this.entry.compileOptions.permalink;
}
return true;
}
static shouldSpiderJavaScriptDependencies(entry) {
if (
entry.compileOptions &&
"spiderJavaScriptDependencies" in entry.compileOptions
) {
return entry.compileOptions.spiderJavaScriptDependencies;
}
return false;
}
}
module.exports = CustomEngine;