diff options
Diffstat (limited to 'node_modules/@11ty/eleventy/src/Template.js')
| -rwxr-xr-x | node_modules/@11ty/eleventy/src/Template.js | 1200 |
1 files changed, 1200 insertions, 0 deletions
diff --git a/node_modules/@11ty/eleventy/src/Template.js b/node_modules/@11ty/eleventy/src/Template.js new file mode 100755 index 0000000..64c4709 --- /dev/null +++ b/node_modules/@11ty/eleventy/src/Template.js @@ -0,0 +1,1200 @@ +import util from "node:util"; +import os from "node:os"; +import path from "node:path"; +import fs from "node:fs"; + +import lodash from "@11ty/lodash-custom"; +import { DateTime } from "luxon"; +import { TemplatePath, isPlainObject } from "@11ty/eleventy-utils"; +import debugUtil from "debug"; +import chalk from "kleur"; + +import ConsoleLogger from "./Util/ConsoleLogger.js"; +import getDateFromGitLastUpdated from "./Util/DateGitLastUpdated.js"; +import getDateFromGitFirstAdded from "./Util/DateGitFirstAdded.js"; +import TemplateData from "./Data/TemplateData.js"; +import TemplateContent from "./TemplateContent.js"; +import TemplatePermalink from "./TemplatePermalink.js"; +import TemplateLayout from "./TemplateLayout.js"; +import TemplateFileSlug from "./TemplateFileSlug.js"; +import ComputedData from "./Data/ComputedData.js"; +import Pagination from "./Plugins/Pagination.js"; +import TemplateBehavior from "./TemplateBehavior.js"; +import TemplateContentPrematureUseError from "./Errors/TemplateContentPrematureUseError.js"; +import TemplateContentUnrenderedTemplateError from "./Errors/TemplateContentUnrenderedTemplateError.js"; +import EleventyBaseError from "./Errors/EleventyBaseError.js"; +import ReservedData from "./Util/ReservedData.js"; +import TransformsUtil from "./Util/TransformsUtil.js"; +import { FileSystemManager } from "./Util/FileSystemManager.js"; + +const { set: lodashSet, get: lodashGet } = lodash; +const fsStat = util.promisify(fs.stat); + +const debug = debugUtil("Eleventy:Template"); +const debugDev = debugUtil("Dev:Eleventy:Template"); + +class Template extends TemplateContent { + #logger; + #fsManager; + + constructor(templatePath, templateData, extensionMap, config) { + debugDev("new Template(%o)", templatePath); + super(templatePath, config); + + this.parsed = path.parse(templatePath); + + // for pagination + this.extraOutputSubdirectory = ""; + + this.extensionMap = extensionMap; + this.templateData = templateData; + this.#initFileSlug(); + + this.linters = []; + this.transforms = {}; + + this.isVerbose = true; + this.isDryRun = false; + this.writeCount = 0; + + this.fileSlug = new TemplateFileSlug(this.inputPath, this.extensionMap, this.eleventyConfig); + this.fileSlugStr = this.fileSlug.getSlug(); + this.filePathStem = this.fileSlug.getFullPathWithoutExtension(); + + this.outputFormat = "fs"; + + this.behavior = new TemplateBehavior(this.config); + this.behavior.setOutputFormat(this.outputFormat); + } + + #initFileSlug() { + this.fileSlug = new TemplateFileSlug(this.inputPath, this.extensionMap, this.eleventyConfig); + this.fileSlugStr = this.fileSlug.getSlug(); + this.filePathStem = this.fileSlug.getFullPathWithoutExtension(); + } + + /* mimic constructor arg order */ + resetCachedTemplate({ templateData, extensionMap, eleventyConfig }) { + super.resetCachedTemplate({ eleventyConfig }); + this.templateData = templateData; + this.extensionMap = extensionMap; + // this.#fsManager = undefined; + this.#initFileSlug(); + } + + get fsManager() { + if (!this.#fsManager) { + this.#fsManager = new FileSystemManager(this.eleventyConfig); + } + return this.#fsManager; + } + + get logger() { + if (!this.#logger) { + this.#logger = new ConsoleLogger(); + this.#logger.isVerbose = this.isVerbose; + } + return this.#logger; + } + + /* Setter for Logger */ + set logger(logger) { + this.#logger = logger; + } + + isRenderable() { + return this.behavior.isRenderable(); + } + + isRenderableDisabled() { + return this.behavior.isRenderableDisabled(); + } + + isRenderableOptional() { + // A template that is lazily rendered once if used by a second order dependency of another template dependency. + // e.g. You change firstpost.md, which is used by feed.xml, but secondpost.md (also used by feed.xml) + // has not yet rendered and needs to be rendered once to populate the cache. + return this.behavior.isRenderableOptional(); + } + + setRenderableOverride(renderableOverride) { + this.behavior.setRenderableOverride(renderableOverride); + } + + reset() { + this.renderCount = 0; + this.writeCount = 0; + } + + resetCaches(types) { + types = this.getResetTypes(types); + + super.resetCaches(types); + + if (types.data) { + delete this._dataCache; + // delete this._usePermalinkRoot; + // delete this._stats; + } + + if (types.render) { + delete this._cacheRenderedPromise; + delete this._cacheRenderedTransformsAndLayoutsPromise; + } + } + + setOutputFormat(to) { + this.outputFormat = to; + this.behavior.setOutputFormat(to); + } + + setIsVerbose(isVerbose) { + this.isVerbose = isVerbose; + this.logger.isVerbose = isVerbose; + } + + setDryRunViaIncremental(isIncremental) { + this.isDryRun = isIncremental; + this.isIncremental = isIncremental; + } + + setDryRun(isDryRun) { + this.isDryRun = !!isDryRun; + } + + setExtraOutputSubdirectory(dir) { + this.extraOutputSubdirectory = dir + "/"; + } + + getTemplateSubfolder() { + let dir = TemplatePath.absolutePath(this.parsed.dir); + let inputDir = TemplatePath.absolutePath(this.inputDir); + return TemplatePath.stripLeadingSubPath(dir, inputDir); + } + + templateUsesLayouts(pageData) { + if (this.hasTemplateRender()) { + return pageData?.[this.config.keys.layout] && this.templateRender.engine.useLayouts(); + } + + // If `layout` prop is set, default to true when engine is unknown + return Boolean(pageData?.[this.config.keys.layout]); + } + + getLayout(layoutKey) { + // already cached downstream in TemplateLayout -> TemplateCache + try { + return TemplateLayout.getTemplate(layoutKey, this.eleventyConfig, this.extensionMap); + } catch (e) { + throw new EleventyBaseError( + `Problem creating an Eleventy Layout for the "${this.inputPath}" template file.`, + e, + ); + } + } + + get baseFile() { + return this.extensionMap.removeTemplateExtension(this.parsed.base); + } + + async _getRawPermalinkInstance(permalinkValue) { + let perm = new TemplatePermalink(permalinkValue, this.extraOutputSubdirectory); + perm.setUrlTransforms(this.config.urlTransforms); + + this.behavior.setFromPermalink(perm); + + return perm; + } + + async _getLink(data) { + if (!data) { + throw new Error("Internal error: data argument missing in Template->_getLink"); + } + + let permalink = + data[this.config.keys.permalink] ?? + data?.[this.config.keys.computed]?.[this.config.keys.permalink]; + let permalinkValue; + + // `permalink: false` means render but no file system write, e.g. use in collections only) + // `permalink: true` throws an error + if (typeof permalink === "boolean") { + debugDev("Using boolean permalink %o", permalink); + permalinkValue = permalink; + } else if (permalink && (!this.config.dynamicPermalinks || data.dynamicPermalink === false)) { + debugDev("Not using dynamic permalinks, using %o", permalink); + permalinkValue = permalink; + } else if (isPlainObject(permalink)) { + // Empty permalink {} object should act as if no permalink was set at all + // and inherit the default behavior + let isEmptyObject = Object.keys(permalink).length === 0; + if (!isEmptyObject) { + let promises = []; + let keys = []; + for (let key in permalink) { + keys.push(key); + if (key !== "build" && Array.isArray(permalink[key])) { + promises.push( + Promise.all([...permalink[key]].map((entry) => super.renderPermalink(entry, data))), + ); + } else { + promises.push(super.renderPermalink(permalink[key], data)); + } + } + + let results = await Promise.all(promises); + + permalinkValue = {}; + for (let j = 0, k = keys.length; j < k; j++) { + let key = keys[j]; + permalinkValue[key] = results[j]; + debug( + "Rendering permalink.%o for %o: %s becomes %o", + key, + this.inputPath, + permalink[key], + results[j], + ); + } + } + } else if (permalink) { + // render variables inside permalink front matter, bypass markdown + permalinkValue = await super.renderPermalink(permalink, data); + debug("Rendering permalink for %o: %s becomes %o", this.inputPath, permalink, permalinkValue); + debugDev("Permalink rendered with data: %o", data); + } + + // Override default permalink behavior. Only do this if permalink was _not_ in the data cascade + if (!permalink && this.config.dynamicPermalinks && data.dynamicPermalink !== false) { + let tr = await this.getTemplateRender(); + let permalinkCompilation = tr.engine.permalinkNeedsCompilation(""); + if (typeof permalinkCompilation === "function") { + let ret = await this._renderFunction(permalinkCompilation, permalinkValue, this.inputPath); + if (ret !== undefined) { + if (typeof ret === "function") { + // function + permalinkValue = await this._renderFunction(ret, data); + } else { + // scalar + permalinkValue = ret; + } + } + } + } + + if (permalinkValue !== undefined) { + return this._getRawPermalinkInstance(permalinkValue); + } + + // No `permalink` specified in data cascade, do the default + let p = TemplatePermalink.generate( + this.getTemplateSubfolder(), + this.baseFile, + this.extraOutputSubdirectory, + this.engine.defaultTemplateFileExtension, + ); + p.setUrlTransforms(this.config.urlTransforms); + return p; + } + + async usePermalinkRoot() { + // @cachedproperty + if (this._usePermalinkRoot === undefined) { + // TODO this only works with immediate front matter and not data files + let { data } = await this.getFrontMatterData(); + this._usePermalinkRoot = data[this.config.keys.permalinkRoot]; + } + + return this._usePermalinkRoot; + } + + async getOutputLocations(data) { + this.bench.get("(count) getOutputLocations").incrementCount(); + let link = await this._getLink(data); + + let path; + if (await this.usePermalinkRoot()) { + path = link.toPathFromRoot(); + } else { + path = link.toPath(this.outputDir); + } + + return { + linkInstance: link, + rawPath: link.toOutputPath(), + href: link.toHref(), + path: path, + }; + } + + // This is likely now a test-only method + // Preferred to use the singular `getOutputLocations` above. + async getRawOutputPath(data) { + this.bench.get("(count) getRawOutputPath").incrementCount(); + let link = await this._getLink(data); + return link.toOutputPath(); + } + + // Preferred to use the singular `getOutputLocations` above. + async getOutputHref(data) { + this.bench.get("(count) getOutputHref").incrementCount(); + let link = await this._getLink(data); + return link.toHref(); + } + + // Preferred to use the singular `getOutputLocations` above. + async getOutputPath(data) { + this.bench.get("(count) getOutputPath").incrementCount(); + let link = await this._getLink(data); + if (await this.usePermalinkRoot()) { + return link.toPathFromRoot(); + } + return link.toPath(this.outputDir); + } + + async _testGetAllLayoutFrontMatterData() { + let { data: frontMatterData } = await this.getFrontMatterData(); + + if (frontMatterData[this.config.keys.layout]) { + let layout = this.getLayout(frontMatterData[this.config.keys.layout]); + return await layout.getData(); + } + return {}; + } + + async #getData() { + debugDev("%o getData", this.inputPath); + let localData = {}; + let globalData = {}; + + if (this.templateData) { + localData = await this.templateData.getTemplateDirectoryData(this.inputPath); + globalData = await this.templateData.getGlobalData(this.inputPath); + debugDev("%o getData getTemplateDirectoryData and getGlobalData", this.inputPath); + } + + let { data: frontMatterData } = await this.getFrontMatterData(); + + let mergedLayoutData = {}; + let tr = await this.getTemplateRender(); + if (tr.engine.useLayouts()) { + let layoutKey = + frontMatterData[this.config.keys.layout] || + localData[this.config.keys.layout] || + globalData[this.config.keys.layout]; + + // Layout front matter data + if (layoutKey) { + let layout = this.getLayout(layoutKey); + + mergedLayoutData = await layout.getData(); + debugDev("%o getData merged layout chain front matter", this.inputPath); + } + } + + try { + let mergedData = TemplateData.mergeDeep( + this.config.dataDeepMerge, + {}, + globalData, + mergedLayoutData, + localData, + frontMatterData, + ); + + if (this.config.freezeReservedData) { + ReservedData.check(mergedData); + } + + await this.addPage(mergedData); + + debugDev("%o getData mergedData", this.inputPath); + + return mergedData; + } catch (e) { + if ( + ReservedData.isReservedDataError(e) || + (e instanceof TypeError && + e.message.startsWith("Cannot add property") && + e.message.endsWith("not extensible")) + ) { + throw new EleventyBaseError( + `You attempted to set one of Eleventy’s reserved data property names${e.reservedNames ? `: ${e.reservedNames.join(", ")}` : ""}. You can opt-out of this behavior with \`eleventyConfig.setFreezeReservedData(false)\` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. \`eleventy\`, \`pkg\`, and others). Learn more: https://v3.11ty.dev/docs/data-eleventy-supplied/`, + e, + ); + } + + throw e; + } + } + + async getData() { + if (!this._dataCache) { + // @cachedproperty + this._dataCache = this.#getData(); + } + + return this._dataCache; + } + + async addPage(data) { + if (!("page" in data)) { + data.page = {}; + } + + // Make sure to keep these keys synchronized in src/Util/ReservedData.js + data.page.inputPath = this.inputPath; + data.page.fileSlug = this.fileSlugStr; + data.page.filePathStem = this.filePathStem; + data.page.outputFileExtension = this.engine.defaultTemplateFileExtension; + data.page.templateSyntax = this.templateRender.getEnginesList( + data[this.config.keys.engineOverride], + ); + + let newDate = await this.getMappedDate(data); + // Skip date assignment if custom date is falsy. + if (newDate) { + data.page.date = newDate; + } + + // data.page.url + // data.page.outputPath + // data.page.excerpt from gray-matter and Front Matter + // data.page.lang from I18nPlugin + } + + // Tests only + async render() { + throw new Error("Internal error: `Template->render` was removed in Eleventy 3.0."); + } + + // Tests only + async renderLayout() { + throw new Error("Internal error: `Template->renderLayout` was removed in Eleventy 3.0."); + } + + async renderDirect(str, data, bypassMarkdown) { + return super.render(str, data, bypassMarkdown); + } + + // This is the primary render mechanism, called via TemplateMap->populateContentDataInMap + async renderPageEntryWithoutLayout(pageEntry) { + // @cachedproperty + if (!this._cacheRenderedPromise) { + this._cacheRenderedPromise = this.renderDirect(pageEntry.rawInput, pageEntry.data); + this.renderCount++; + } + + return this._cacheRenderedPromise; + } + + setLinters(linters) { + if (!isPlainObject(linters)) { + throw new Error("Object expected in setLinters"); + } + // this acts as a reset + this.linters = []; + for (let linter of Object.values(linters).filter((l) => typeof l === "function")) { + this.addLinter(linter); + } + } + + addLinter(callback) { + this.linters.push(callback); + } + + async runLinters(str, page) { + let { inputPath, outputPath, url } = page; + let pageData = page.data.page; + + for (let linter of this.linters) { + // these can be asynchronous but no guarantee of order when they run + linter.call( + { + inputPath, + outputPath, + url, + page: pageData, + }, + str, + inputPath, + outputPath, + ); + } + } + + setTransforms(transforms) { + if (!isPlainObject(transforms)) { + throw new Error("Object expected in setTransforms"); + } + this.transforms = transforms; + } + + async runTransforms(str, pageEntry) { + return TransformsUtil.runAll(str, pageEntry.data.page, this.transforms, { + logger: this.logger, + }); + } + + async #renderComputedUnit(entry, data) { + if (typeof entry === "string") { + return this.renderComputedData(entry, data); + } + + if (isPlainObject(entry)) { + for (let key in entry) { + entry[key] = await this.#renderComputedUnit(entry[key], data); + } + } + + if (Array.isArray(entry)) { + for (let j = 0, k = entry.length; j < k; j++) { + entry[j] = await this.#renderComputedUnit(entry[j], data); + } + } + + return entry; + } + + _addComputedEntry(computedData, obj, parentKey, declaredDependencies) { + // this check must come before isPlainObject + if (typeof obj === "function") { + computedData.add(parentKey, obj, declaredDependencies); + } else if (Array.isArray(obj) || typeof obj === "string") { + // Arrays are treated as one entry in the dependency graph now, Issue #3728 + computedData.addTemplateString( + parentKey, + async function (innerData) { + return this.tmpl.#renderComputedUnit(obj, innerData); + }, + declaredDependencies, + this.getParseForSymbolsFunction(obj), + this, + ); + } else if (isPlainObject(obj)) { + // Arrays used to be computed here + for (let key in obj) { + let keys = []; + if (parentKey) { + keys.push(parentKey); + } + keys.push(key); + this._addComputedEntry(computedData, obj[key], keys.join("."), declaredDependencies); + } + } else { + // Numbers, booleans, etc + computedData.add(parentKey, obj, declaredDependencies); + } + } + + async addComputedData(data) { + if (isPlainObject(data?.[this.config.keys.computed])) { + this.computedData = new ComputedData(this.config); + + // Note that `permalink` is only a thing that gets consumed—it does not go directly into generated data + // this allows computed entries to use page.url or page.outputPath and they’ll be resolved properly + + // TODO Room for optimization here—we don’t need to recalculate `getOutputHref` and `getOutputPath` + // TODO Why are these using addTemplateString instead of add + this.computedData.addTemplateString( + "page.url", + async function (data) { + return this.tmpl.getOutputHref(data); + }, + data.permalink ? ["permalink"] : undefined, + false, // skip symbol resolution + this, + ); + + this.computedData.addTemplateString( + "page.outputPath", + async function (data) { + return this.tmpl.getOutputPath(data); + }, + data.permalink ? ["permalink"] : undefined, + false, // skip symbol resolution + this, + ); + + // Check for reserved properties in computed data + if (this.config.freezeReservedData) { + ReservedData.check(data[this.config.keys.computed]); + } + + // actually add the computed data + this._addComputedEntry(this.computedData, data[this.config.keys.computed]); + + // limited run of computed data—save the stuff that relies on collections for later. + debug("First round of computed data for %o", this.inputPath); + await this.computedData.setupData(data, function (entry) { + return !this.isUsesStartsWith(entry, "collections."); + + // TODO possible improvement here is to only process page.url, page.outputPath, permalink + // instead of only punting on things that rely on collections. + // let firstPhaseComputedData = ["page.url", "page.outputPath", ...this.getOrderFor("page.url"), ...this.getOrderFor("page.outputPath")]; + // return firstPhaseComputedData.indexOf(entry) > -1; + }); + } else { + if (!("page" in data)) { + data.page = {}; + } + + // pagination will already have these set via Pagination->getPageTemplates + if (data.page.url && data.page.outputPath) { + return; + } + + let { href, path } = await this.getOutputLocations(data); + data.page.url = href; + data.page.outputPath = path; + } + } + + // Computed data consuming collections! + async resolveRemainingComputedData(data) { + // If it doesn’t exist, computed data is not used for this template + if (this.computedData) { + debug("Second round of computed data for %o", this.inputPath); + return this.computedData.processRemainingData(data); + } + } + + static augmentWithTemplateContentProperty(obj) { + return Object.defineProperties(obj, { + needsCheck: { + enumerable: false, + writable: true, + value: true, + }, + _templateContent: { + enumerable: false, + writable: true, + value: undefined, + }, + templateContent: { + enumerable: true, + set(content) { + if (content === undefined) { + this.needsCheck = false; + } + this._templateContent = content; + }, + get() { + if (this.needsCheck && this._templateContent === undefined) { + if (this.template.isRenderable()) { + // should at least warn here + throw new TemplateContentPrematureUseError( + `Tried to use templateContent too early on ${this.inputPath}${ + this.pageNumber ? ` (page ${this.pageNumber})` : "" + }`, + ); + } else { + throw new TemplateContentUnrenderedTemplateError( + `Tried to use templateContent on unrendered template: ${ + this.inputPath + }${this.pageNumber ? ` (page ${this.pageNumber})` : ""}`, + ); + } + } + return this._templateContent; + }, + }, + // Alias for templateContent for consistency + content: { + enumerable: true, + get() { + return this.templateContent; + }, + set() { + throw new Error("Setter not available for `content`. Use `templateContent` instead."); + }, + }, + }); + } + + static async runPreprocessors(inputPath, content, data, preprocessors) { + let skippedVia = false; + for (let [name, preprocessor] of Object.entries(preprocessors)) { + let { filter, callback } = preprocessor; + + let filters; + if (Array.isArray(filter)) { + filters = filter; + } else if (typeof filter === "string") { + filters = filter.split(","); + } else { + throw new Error( + `Expected file extensions passed to "${name}" content preprocessor to be a string or array. Received: ${filter}`, + ); + } + + filters = filters.map((extension) => { + if (extension.startsWith(".") || extension === "*") { + return extension; + } + + return `.${extension}`; + }); + + if (!filters.some((extension) => extension === "*" || inputPath.endsWith(extension))) { + // skip + continue; + } + + try { + let ret = await callback.call( + { + inputPath, + }, + data, + content, + ); + + // Returning explicit false is the same as ignoring the template + if (ret === false) { + skippedVia = name; + continue; + } + + // Different from transforms: returning falsy (not false) here does nothing (skips the preprocessor) + if (ret) { + content = ret; + } + } catch (e) { + throw new EleventyBaseError( + `Preprocessor \`${name}\` encountered an error when transforming ${inputPath}.`, + e, + ); + } + } + + return { + skippedVia, + content, + }; + } + + async getTemplates(data) { + let content = await this.getPreRender(); + let { skippedVia, content: rawInput } = await Template.runPreprocessors( + this.inputPath, + content, + data, + this.config.preprocessors, + ); + + if (skippedVia) { + debug( + "Skipping %o, the %o preprocessor returned an explicit `false`", + this.inputPath, + skippedVia, + ); + return []; + } + + // Raw Input *includes* preprocessor modifications + // https://github.com/11ty/eleventy/issues/1206 + data.page.rawInput = rawInput; + + if (!Pagination.hasPagination(data)) { + await this.addComputedData(data); + + let obj = { + template: this, // not on the docs but folks are relying on it + rawInput, + groupNumber: 0, // i18n plugin + data, + + page: data.page, + inputPath: this.inputPath, + fileSlug: this.fileSlugStr, + filePathStem: this.filePathStem, + date: data.page.date, + outputPath: data.page.outputPath, + url: data.page.url, + }; + + obj = Template.augmentWithTemplateContentProperty(obj); + + return [obj]; + } else { + // needs collections for pagination items + // but individual pagination entries won’t be part of a collection + this.paging = new Pagination(this, data, this.config); + + let pageTemplates = await this.paging.getPageTemplates(); + let objects = []; + + for (let pageEntry of pageTemplates) { + await pageEntry.template.addComputedData(pageEntry.data); + + let obj = { + template: pageEntry.template, // not on the docs but folks are relying on it + rawInput, + pageNumber: pageEntry.pageNumber, + groupNumber: pageEntry.groupNumber || 0, + + data: pageEntry.data, + + inputPath: this.inputPath, + fileSlug: this.fileSlugStr, + filePathStem: this.filePathStem, + + page: pageEntry.data.page, + date: pageEntry.data.page.date, + outputPath: pageEntry.data.page.outputPath, + url: pageEntry.data.page.url, + }; + + obj = Template.augmentWithTemplateContentProperty(obj); + + objects.push(obj); + } + + return objects; + } + } + + async _write({ url, outputPath, data, rawInput }, finalContent) { + let lang = { + start: "Writing", + finished: "written", + }; + + if (!this.isDryRun) { + if (this.logger.isLoggingEnabled()) { + let isVirtual = this.isVirtualTemplate(); + let tr = await this.getTemplateRender(); + let engineList = tr.getReadableEnginesListDifferingFromFileExtension(); + let suffix = `${isVirtual ? " (virtual)" : ""}${engineList ? ` (${engineList})` : ""}`; + this.logger.log( + `${lang.start} ${outputPath} ${chalk.gray(`from ${this.inputPath}${suffix}`)}`, + ); + } + } else if (this.isDryRun) { + return; + } + + let templateBenchmarkDir = this.bench.get("Template make parent directory"); + templateBenchmarkDir.before(); + + if (this.eleventyConfig.templateHandling?.writeMode === "async") { + await this.fsManager.createDirectoryForFile(outputPath); + } else { + this.fsManager.createDirectoryForFileSync(outputPath); + } + + templateBenchmarkDir.after(); + + if (!Buffer.isBuffer(finalContent) && typeof finalContent !== "string") { + throw new Error( + `The return value from the render function for the ${this.engine.name} template was not a String or Buffer. Received ${finalContent}`, + ); + } + + let templateBenchmark = this.bench.get("Template Write"); + templateBenchmark.before(); + + if (this.eleventyConfig.templateHandling?.writeMode === "async") { + await this.fsManager.writeFile(outputPath, finalContent); + } else { + this.fsManager.writeFileSync(outputPath, finalContent); + } + + templateBenchmark.after(); + this.writeCount++; + debug(`${outputPath} ${lang.finished}.`); + + let ret = { + inputPath: this.inputPath, + outputPath: outputPath, + url, + content: finalContent, + rawInput, + }; + + if (data && this.config.dataFilterSelectors?.size > 0) { + ret.data = this.retrieveDataForJsonOutput(data, this.config.dataFilterSelectors); + } + + return ret; + } + + async #renderPageEntryWithLayoutsAndTransforms(pageEntry) { + let content; + let layoutKey = pageEntry.data[this.config.keys.layout]; + if (this.engine.useLayouts() && layoutKey) { + let layout = pageEntry.template.getLayout(layoutKey); + content = await layout.renderPageEntry(pageEntry); + } else { + content = pageEntry.templateContent; + } + + await this.runLinters(content, pageEntry); + + content = await this.runTransforms(content, pageEntry); + return content; + } + + async renderPageEntry(pageEntry) { + // @cachedproperty + if (!pageEntry.template._cacheRenderedTransformsAndLayoutsPromise) { + pageEntry.template._cacheRenderedTransformsAndLayoutsPromise = + this.#renderPageEntryWithLayoutsAndTransforms(pageEntry); + } + + return pageEntry.template._cacheRenderedTransformsAndLayoutsPromise; + } + + retrieveDataForJsonOutput(data, selectors) { + let filtered = {}; + for (let selector of selectors) { + let value = lodashGet(data, selector); + lodashSet(filtered, selector, value); + } + return filtered; + } + + async generateMapEntry(mapEntry, to) { + let ret = []; + + for (let page of mapEntry._pages) { + let content; + + // Note that behavior.render is overridden when using json or ndjson output + if (page.template.isRenderable()) { + // this reuses page.templateContent, it doesn’t render it + content = await page.template.renderPageEntry(page); + } + + if (to === "json" || to === "ndjson") { + let obj = { + url: page.url, + inputPath: page.inputPath, + outputPath: page.outputPath, + rawInput: page.rawInput, + content: content, + }; + + if (this.config.dataFilterSelectors?.size > 0) { + obj.data = this.retrieveDataForJsonOutput(page.data, this.config.dataFilterSelectors); + } + + if (to === "ndjson") { + let jsonString = JSON.stringify(obj); + this.logger.toStream(jsonString + os.EOL); + continue; + } + + // json + ret.push(obj); + continue; + } + + if (!page.template.isRenderable()) { + debug("Template not written %o from %o.", page.outputPath, page.template.inputPath); + continue; + } + + if (!page.template.behavior.isWriteable()) { + debug( + "Template not written %o from %o (via permalink: false, permalink.build: false, or a permalink object without a build property).", + page.outputPath, + page.template.inputPath, + ); + continue; + } + + // compile returned undefined + if (content !== undefined) { + ret.push(this._write(page, content)); + } + } + + return Promise.all(ret); + } + + async clone() { + // TODO do we need to even run the constructor here or can we simplify it even more + let tmpl = new Template( + this.inputPath, + this.templateData, + this.extensionMap, + this.eleventyConfig, + ); + + // We use this cheap property setter below instead + // await tmpl.getTemplateRender(); + + // preserves caches too, e.g. _frontMatterDataCache + // Does not yet include .computedData + for (let key in this) { + tmpl[key] = this[key]; + } + + return tmpl; + } + + getWriteCount() { + return this.writeCount; + } + + getRenderCount() { + return this.renderCount; + } + + async getInputFileStat() { + // @cachedproperty + if (!this._stats) { + this._stats = fsStat(this.inputPath); + } + + return this._stats; + } + + async _getDateInstance(key = "birthtimeMs") { + let stat = await this.getInputFileStat(); + + // Issue 1823: https://github.com/11ty/eleventy/issues/1823 + // return current Date in a Lambda + // otherwise ctime would be "1980-01-01T00:00:00.000Z" + // otherwise birthtime would be "1970-01-01T00:00:00.000Z" + if (stat.birthtimeMs === 0) { + return new Date(); + } + + let newDate = new Date(stat[key]); + + debug( + "Template date: using file’s %o for %o of %o (from %o)", + key, + this.inputPath, + newDate, + stat.birthtimeMs, + ); + + return newDate; + } + + async getMappedDate(data) { + let dateValue = data?.date; + + // These can return a Date object, or a string. + // Already type checked to be functions in UserConfig + for (let fn of this.config.customDateParsing) { + let ret = fn.call( + { + page: data.page, + }, + dateValue, + ); + + if (ret) { + debug("getMappedDate: date value override via `addDateParsing` callback to %o", ret); + dateValue = ret; + } + } + + if (dateValue) { + debug("getMappedDate: using a date in the data for %o of %o", this.inputPath, data.date); + if (dateValue?.constructor?.name === "DateTime") { + // YAML does its own date parsing + debug("getMappedDate: found DateTime instance: %o", dateValue); + return dateValue.toJSDate(); + } + + if (dateValue instanceof Date) { + // YAML does its own date parsing + debug("getMappedDate: found Date instance (maybe from YAML): %o", dateValue); + return dateValue; + } + + if (typeof dateValue !== "string") { + throw new Error( + `Data cascade value for \`date\` (${dateValue}) is invalid for ${this.inputPath}. Expected a JavaScript Date instance, luxon DateTime instance, or String value.`, + ); + } + + // special strings + if (!this.isVirtualTemplate()) { + if (dateValue.toLowerCase() === "git last modified") { + let d = await getDateFromGitLastUpdated(this.inputPath); + if (d) { + return d; + } + + // return now if this file is not yet available in `git` + return new Date(); + } + if (dateValue.toLowerCase() === "last modified") { + return this._getDateInstance("ctimeMs"); + } + if (dateValue.toLowerCase() === "git created") { + let d = await getDateFromGitFirstAdded(this.inputPath); + if (d) { + return d; + } + + // return now if this file is not yet available in `git` + return new Date(); + } + if (dateValue.toLowerCase() === "created") { + return this._getDateInstance("birthtimeMs"); + } + } + + // try to parse with Luxon + let date = DateTime.fromISO(dateValue, { zone: "utc" }); + if (!date.isValid) { + throw new Error( + `Data cascade value for \`date\` (${dateValue}) is invalid for ${this.inputPath}`, + ); + } + debug("getMappedDate: Luxon parsed %o: %o and %o", dateValue, date, date.toJSDate()); + + return date.toJSDate(); + } + + // No Date supplied in the Data Cascade, try to find the date in the file name + let filepathRegex = this.inputPath.match(/(\d{4}-\d{2}-\d{2})/); + if (filepathRegex !== null) { + // if multiple are found in the path, use the first one for the date + let dateObj = DateTime.fromISO(filepathRegex[1], { + zone: "utc", + }).toJSDate(); + debug( + "getMappedDate: using filename regex time for %o of %o: %o", + this.inputPath, + filepathRegex[1], + dateObj, + ); + return dateObj; + } + + // No Date supplied in the Data Cascade + if (this.isVirtualTemplate()) { + return new Date(); + } + + return this._getDateInstance("birthtimeMs"); + } + + // Important reminder: Template data is first generated in TemplateMap + async getTemplateMapEntries(data) { + debugDev("%o getMapped()", this.inputPath); + + this.behavior.setRenderViaDataCascade(data); + + let entries = []; + // does not return outputPath or url, we don’t want to render permalinks yet + entries.push({ + template: this, + inputPath: this.inputPath, + data, + }); + + return entries; + } +} + +export default Template; |
