import { TemplatePath } from "@11ty/eleventy-utils"; class EleventyExtensionMap { #engineManager; constructor(config) { this.setTemplateConfig(config); this._spiderJsDepsCache = {}; /** @type {Array} */ this.validTemplateLanguageKeys; } setFormats(formatKeys = []) { // raw this.formatKeys = formatKeys; this.unfilteredFormatKeys = formatKeys.map(function (key) { return key.trim().toLowerCase(); }); this.validTemplateLanguageKeys = this.unfilteredFormatKeys.filter((key) => this.hasExtension(key), ); this.passthroughCopyKeys = this.unfilteredFormatKeys.filter((key) => !this.hasExtension(key)); } setTemplateConfig(config) { if (!config || config.constructor.name !== "TemplateConfig") { throw new Error("Internal error: Missing or invalid `config` argument."); } this.templateConfig = config; } get config() { return this.templateConfig.getConfig(); } get engineManager() { if (!this.#engineManager) { throw new Error("Internal error: Missing `#engineManager` in EleventyExtensionMap."); } return this.#engineManager; } set engineManager(mgr) { this.#engineManager = mgr; } reset() { this.#engineManager.reset(); } /* Used for layout path resolution */ getFileList(path, dir) { if (!path) { return []; } let files = []; this.validTemplateLanguageKeys.forEach((key) => { this.getExtensionsFromKey(key).forEach(function (extension) { files.push((dir ? dir + "/" : "") + path + "." + extension); }); }); return files; } // Warning: this would false positive on an include, but is only used // on paths found from the file system glob search. // TODO: Method name might just need to be renamed to something more accurate. isFullTemplateFilePath(path) { for (let extension of this.validTemplateLanguageKeys) { if (path.endsWith(`.${extension}`)) { return true; } } return false; } getCustomExtensionEntry(extension) { if (!this.config.extensionMap) { return; } for (let entry of this.config.extensionMap) { if (entry.extension === extension) { return entry; } } } getValidExtensionsForPath(path) { let extensions = new Set(); for (let extension in this.extensionToKeyMap) { if (path.endsWith(`.${extension}`)) { extensions.add(extension); } } // if multiple extensions are valid, sort from longest to shortest // e.g. .11ty.js and .js let sorted = Array.from(extensions) .filter((extension) => this.validTemplateLanguageKeys.includes(extension)) .sort((a, b) => b.length - a.length); return sorted; } async shouldSpiderJavaScriptDependencies(path) { let extensions = this.getValidExtensionsForPath(path); for (let extension of extensions) { if (extension in this._spiderJsDepsCache) { return this._spiderJsDepsCache[extension]; } let cls = await this.engineManager.getEngineClassByExtension(extension); if (cls) { let entry = this.getCustomExtensionEntry(extension); let shouldSpider = cls.shouldSpiderJavaScriptDependencies(entry); this._spiderJsDepsCache[extension] = shouldSpider; return shouldSpider; } } return false; } getPassthroughCopyGlobs(inputDir) { return this._getGlobs(this.passthroughCopyKeys, inputDir); } getValidGlobs(inputDir) { return this._getGlobs(this.validTemplateLanguageKeys, inputDir); } getGlobs(inputDir) { return this._getGlobs(this.unfilteredFormatKeys, inputDir); } _getGlobs(formatKeys, inputDir = "") { let extensions = new Set(); for (let key of formatKeys) { if (this.hasExtension(key)) { for (let extension of this.getExtensionsFromKey(key)) { extensions.add(extension); } } else { extensions.add(key); } } let dir = TemplatePath.convertToRecursiveGlobSync(inputDir); if (extensions.size === 1) { return [`${dir}/*.${Array.from(extensions)[0]}`]; } else if (extensions.size > 1) { return [ // extra curly brackets /*.{cjs,txt} `${dir}/*.{${Array.from(extensions).join(",")}}`, ]; } return []; } hasExtension(key) { for (let extension in this.extensionToKeyMap) { if ( this.extensionToKeyMap[extension].key === key || this.extensionToKeyMap[extension].aliasKey === key ) { return true; } } return false; } getExtensionsFromKey(key) { let extensions = new Set(); for (let extension in this.extensionToKeyMap) { if (this.extensionToKeyMap[extension].aliasKey) { // only add aliased extension if explicitly referenced in formats // overrides will not have an aliasKey (md => md) if (this.extensionToKeyMap[extension].aliasKey === key) { extensions.add(extension); } } else if (this.extensionToKeyMap[extension].key === key) { extensions.add(extension); } } return Array.from(extensions); } // Only `addExtension` configuration API extensions getExtensionEntriesFromKey(key) { let entries = new Set(); if ("extensionMap" in this.config) { for (let entry of this.config.extensionMap) { if (entry.key === key) { entries.add(entry); } } } return Array.from(entries); } // Determines whether a path is a passthrough copy file or a template (via TemplateWriter) hasEngine(pathOrKey) { return !!this.getKey(pathOrKey); } getKey(pathOrKey) { pathOrKey = (pathOrKey || "").toLowerCase(); for (let extension in this.extensionToKeyMap) { if (pathOrKey === extension || pathOrKey.endsWith("." + extension)) { let key = this.extensionToKeyMap[extension].aliasKey || this.extensionToKeyMap[extension].key; // must be a valid format key passed (e.g. via --formats) if (this.validTemplateLanguageKeys.includes(key)) { return key; } } } } getExtensionEntry(pathOrKey) { pathOrKey = (pathOrKey || "").toLowerCase(); for (let extension in this.extensionToKeyMap) { if (pathOrKey === extension || pathOrKey.endsWith("." + extension)) { return this.extensionToKeyMap[extension]; } } } removeTemplateExtension(path) { for (let extension in this.extensionToKeyMap) { if (path === extension || path.endsWith("." + extension)) { return path.slice( 0, path.length - 1 - extension.length < 0 ? 0 : path.length - 1 - extension.length, ); } } return path; } // keys are file extensions // values are template language keys get extensionToKeyMap() { if (!this._extensionToKeyMap) { this._extensionToKeyMap = { md: { key: "md", extension: "md" }, html: { key: "html", extension: "html" }, njk: { key: "njk", extension: "njk" }, liquid: { key: "liquid", extension: "liquid" }, "11ty.js": { key: "11ty.js", extension: "11ty.js" }, "11ty.cjs": { key: "11ty.js", extension: "11ty.cjs" }, "11ty.mjs": { key: "11ty.js", extension: "11ty.mjs" }, }; if ("extensionMap" in this.config) { for (let entry of this.config.extensionMap) { // extension and key are only different when aliasing. this._extensionToKeyMap[entry.extension] = entry; } } } return this._extensionToKeyMap; } getReadableFileExtensions() { return Object.keys(this.extensionToKeyMap).join(" "); } } export default EleventyExtensionMap;