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;