From 7a52ddeba2a68388b544f529d2d92104420f77b0 Mon Sep 17 00:00:00 2001 From: Shipwreckt Date: Fri, 31 Oct 2025 20:02:14 +0000 Subject: Changed from static to 11ty! --- node_modules/@11ty/eleventy/src/TemplateMap.js | 684 +++++++++++++++++++++++++ 1 file changed, 684 insertions(+) create mode 100644 node_modules/@11ty/eleventy/src/TemplateMap.js (limited to 'node_modules/@11ty/eleventy/src/TemplateMap.js') diff --git a/node_modules/@11ty/eleventy/src/TemplateMap.js b/node_modules/@11ty/eleventy/src/TemplateMap.js new file mode 100644 index 0000000..52cceb1 --- /dev/null +++ b/node_modules/@11ty/eleventy/src/TemplateMap.js @@ -0,0 +1,684 @@ +import { isPlainObject, TemplatePath } from "@11ty/eleventy-utils"; +import debugUtil from "debug"; + +import TemplateCollection from "./TemplateCollection.js"; +import EleventyErrorUtil from "./Errors/EleventyErrorUtil.js"; +import UsingCircularTemplateContentReferenceError from "./Errors/UsingCircularTemplateContentReferenceError.js"; +import EleventyBaseError from "./Errors/EleventyBaseError.js"; +import DuplicatePermalinkOutputError from "./Errors/DuplicatePermalinkOutputError.js"; +import TemplateData from "./Data/TemplateData.js"; +import GlobalDependencyMap from "./GlobalDependencyMap.js"; + +const debug = debugUtil("Eleventy:TemplateMap"); + +class EleventyMapPagesError extends EleventyBaseError {} +class EleventyDataSchemaError extends EleventyBaseError {} + +// These template URL filenames are allowed to exclude file extensions +const EXTENSIONLESS_URL_ALLOWLIST = [ + "/_redirects", // Netlify specific + "/.htaccess", // Apache + "/_headers", // Cloudflare +]; + +// must match TemplateDepGraph +const SPECIAL_COLLECTION_NAMES = { + keys: "[keys]", + all: "all", +}; + +class TemplateMap { + #dependencyMapInitialized = false; + + constructor(eleventyConfig) { + if (!eleventyConfig || eleventyConfig.constructor.name !== "TemplateConfig") { + throw new Error("Missing or invalid `eleventyConfig` argument."); + } + this.eleventyConfig = eleventyConfig; + this.map = []; + this.collectionsData = null; + this.cached = false; + this.verboseOutput = true; + this.collection = new TemplateCollection(); + } + + set userConfig(config) { + this._userConfig = config; + } + + get userConfig() { + if (!this._userConfig) { + // TODO use this.config for this, need to add collections to mergeable props in userconfig + this._userConfig = this.eleventyConfig.userConfig; + } + + return this._userConfig; + } + + get config() { + if (!this._config) { + this._config = this.eleventyConfig.getConfig(); + } + return this._config; + } + + async add(template) { + if (!template) { + return; + } + + let data = await template.getData(); + let entries = await template.getTemplateMapEntries(data); + + for (let map of entries) { + this.map.push(map); + } + } + + getMap() { + return this.map; + } + + getTagTarget(str) { + if (str === "collections") { + // special, means targeting `collections` specifically + return SPECIAL_COLLECTION_NAMES.keys; + } + + if (str.startsWith("collections.")) { + return str.slice("collections.".length); + } + + // Fixes #2851 + if (str.startsWith("collections['") || str.startsWith('collections["')) { + return str.slice("collections['".length, -2); + } + } + + getPaginationTagTarget(entry) { + if (entry.data.pagination?.data) { + return this.getTagTarget(entry.data.pagination.data); + } + } + + #addEntryToGlobalDependencyGraph(entry) { + let consumes = []; + consumes.push(this.getPaginationTagTarget(entry)); + + if (Array.isArray(entry.data.eleventyImport?.collections)) { + for (let tag of entry.data.eleventyImport.collections) { + consumes.push(tag); + } + } + + // Important: consumers must come before publishers + + // TODO it’d be nice to set the dependency relationship for addCollection here + // But collections are not yet populated (they populate after template order) + let publishes = TemplateData.getIncludedCollectionNames(entry.data); + + this.config.uses.addNewNodeRelationships(entry.inputPath, consumes, publishes); + } + + addAllToGlobalDependencyGraph() { + this.#dependencyMapInitialized = true; + + // Should come before individual entry additions + this.config.uses.initializeUserConfigurationApiCollections(); + + for (let entry of this.map) { + this.#addEntryToGlobalDependencyGraph(entry); + } + } + + async setCollectionByTagName(tagName) { + if (this.isUserConfigCollectionName(tagName)) { + // async + this.collectionsData[tagName] = await this.getUserConfigCollection(tagName); + } else { + this.collectionsData[tagName] = this.getTaggedCollection(tagName); + } + + let precompiled = this.config.precompiledCollections; + if (precompiled?.[tagName]) { + if ( + tagName === "all" || + !Array.isArray(this.collectionsData[tagName]) || + this.collectionsData[tagName].length === 0 + ) { + this.collectionsData[tagName] = precompiled[tagName]; + } + } + } + + // TODO(slightlyoff): major bottleneck + async initDependencyMap(fullTemplateOrder) { + // Temporary workaround for async constructor work in templates + // Issue #3170 #3870 + let inputPathSet = new Set(fullTemplateOrder); + await Promise.all( + this.map + .filter(({ inputPath }) => { + return inputPathSet.has(inputPath); + }) + .map(({ template }) => { + // This also happens for layouts in TemplateContent->compile + return template.asyncTemplateInitialization(); + }), + ); + + for (let depEntry of fullTemplateOrder) { + if (GlobalDependencyMap.isCollection(depEntry)) { + let tagName = GlobalDependencyMap.getTagName(depEntry); + // [keys] should initialize `all` + if (tagName === SPECIAL_COLLECTION_NAMES.keys) { + await this.setCollectionByTagName("all"); + // [NAME] is special and implied (e.g. [keys]) + } else if (!tagName.startsWith("[") && !tagName.endsWith("]")) { + // is a tag (collection) entry + await this.setCollectionByTagName(tagName); + } + continue; + } + + // is a template entry + let map = this.getMapEntryForInputPath(depEntry); + await this.#initDependencyMapEntry(map); + } + } + + async #initDependencyMapEntry(map) { + try { + map._pages = await map.template.getTemplates(map.data); + } catch (e) { + throw new EleventyMapPagesError( + "Error generating template page(s) for " + map.inputPath + ".", + e, + ); + } + + if (map._pages.length === 0) { + // Reminder: a serverless code path was removed here. + } else { + let counter = 0; + for (let page of map._pages) { + // Copy outputPath to map entry + // This is no longer used internally, just for backwards compatibility + // Error added in v3 for https://github.com/11ty/eleventy/issues/3183 + if (map.data.pagination) { + if (!Object.prototype.hasOwnProperty.call(map, "outputPath")) { + Object.defineProperty(map, "outputPath", { + get() { + throw new Error( + "Internal error: `.outputPath` on a paginated map entry is not consistent. Use `_pages[…].outputPath` instead.", + ); + }, + }); + } + } else if (!map.outputPath) { + map.outputPath = page.outputPath; + } + + if (counter === 0 || map.data.pagination?.addAllPagesToCollections) { + if (map.data.eleventyExcludeFromCollections !== true) { + // is in *some* collections + this.collection.add(page); + } + } + + counter++; + } + } + } + + getTemplateOrder() { + // 1. Templates that don’t use Pagination + // 2. Pagination templates that consume config API collections + // 3. Pagination templates consuming `collections` + // 4. Pagination templates consuming `collections.all` + let fullTemplateOrder = this.config.uses.getTemplateOrder(); + + return fullTemplateOrder + .map((entry) => { + if (GlobalDependencyMap.isCollection(entry)) { + return entry; + } + + let inputPath = TemplatePath.addLeadingDotSlash(entry); + if (!this.hasMapEntryForInputPath(inputPath)) { + return false; + } + return inputPath; + }) + .filter(Boolean); + } + + async cache() { + if (!this.#dependencyMapInitialized) { + this.addAllToGlobalDependencyGraph(); + } + + this.collectionsData = {}; + + for (let entry of this.map) { + entry.data.collections = this.collectionsData; + } + + let fullTemplateOrder = this.getTemplateOrder(); + debug( + "Rendering templates in order (%o concurrency): %O", + this.userConfig.getConcurrency(), + fullTemplateOrder, + ); + + await this.initDependencyMap(fullTemplateOrder); + await this.resolveRemainingComputedData(); + + let orderedPaths = this.#removeTagsFromTemplateOrder(fullTemplateOrder); + + let orderedMap = orderedPaths.map((inputPath) => { + return this.getMapEntryForInputPath(inputPath); + }); + + await this.config.events.emitLazy("eleventy.contentMap", () => { + return { + inputPathToUrl: this.generateInputUrlContentMap(orderedMap), + urlToInputPath: this.generateUrlMap(orderedMap), + }; + }); + + await this.runDataSchemas(orderedMap); + await this.populateContentDataInMap(orderedMap); + + this.populateCollectionsWithContent(); + this.cached = true; + + this.checkForDuplicatePermalinks(); + this.checkForMissingFileExtensions(); + + await this.config.events.emitLazy("eleventy.layouts", () => this.generateLayoutsMap()); + } + + generateInputUrlContentMap(orderedMap) { + let entries = {}; + for (let entry of orderedMap) { + entries[entry.inputPath] = entry._pages.map((entry) => entry.url); + } + return entries; + } + + generateUrlMap(orderedMap) { + let entries = {}; + for (let entry of orderedMap) { + for (let page of entry._pages) { + // duplicate urls throw an error, so we can return non array here + entries[page.url] = { + inputPath: entry.inputPath, + groupNumber: page.groupNumber, + }; + } + } + return entries; + } + + hasMapEntryForInputPath(inputPath) { + return Boolean(this.getMapEntryForInputPath(inputPath)); + } + + // TODO(slightlyoff): hot inner loop? + getMapEntryForInputPath(inputPath) { + let absoluteInputPath = TemplatePath.absolutePath(inputPath); + return this.map.find((entry) => { + if (entry.inputPath === inputPath || entry.inputPath === absoluteInputPath) { + return entry; + } + }); + } + + #removeTagsFromTemplateOrder(maps) { + return maps.filter((dep) => !GlobalDependencyMap.isCollection(dep)); + } + + async runDataSchemas(orderedMap) { + for (let map of orderedMap) { + if (!map._pages) { + continue; + } + + for (let pageEntry of map._pages) { + // Data Schema callback #879 + if (typeof pageEntry.data[this.config.keys.dataSchema] === "function") { + try { + await pageEntry.data[this.config.keys.dataSchema](pageEntry.data); + } catch (e) { + throw new EleventyDataSchemaError( + `Error in the data schema for: ${map.inputPath} (via \`eleventyDataSchema\`)`, + e, + ); + } + } + } + } + } + + async populateContentDataInMap(orderedMap) { + let usedTemplateContentTooEarlyMap = []; + + // Note that empty pagination templates will be skipped here as not renderable + let filteredMap = orderedMap.filter((entry) => entry.template.isRenderable()); + + // Get concurrency level from user config + const concurrency = this.userConfig.getConcurrency(); + + // Process the templates in chunks to limit concurrency + // This replaces the functionality of p-map's concurrency option + for (let i = 0; i < filteredMap.length; i += concurrency) { + // Create a chunk of tasks that will run in parallel + const chunk = filteredMap.slice(i, i + concurrency); + + // Run the chunk of tasks in parallel + await Promise.all( + chunk.map(async (map) => { + if (!map._pages) { + throw new Error(`Internal error: _pages not found for ${map.inputPath}`); + } + + // IMPORTANT: this is where template content is rendered + try { + for (let pageEntry of map._pages) { + pageEntry.templateContent = + await pageEntry.template.renderPageEntryWithoutLayout(pageEntry); + } + } catch (e) { + if (EleventyErrorUtil.isPrematureTemplateContentError(e)) { + // Add to list of templates that need to be processed again + usedTemplateContentTooEarlyMap.push(map); + + // Reset cached render promise + for (let pageEntry of map._pages) { + pageEntry.template.resetCaches({ render: true }); + } + } else { + throw e; + } + } + }), + ); + } + + // Process templates that had premature template content errors + // This is the second pass for templates that couldn't be rendered in the first pass + for (let map of usedTemplateContentTooEarlyMap) { + try { + for (let pageEntry of map._pages) { + pageEntry.templateContent = + await pageEntry.template.renderPageEntryWithoutLayout(pageEntry); + } + } catch (e) { + if (EleventyErrorUtil.isPrematureTemplateContentError(e)) { + // If we still have template content errors after the second pass, + // it's likely a circular reference + throw new UsingCircularTemplateContentReferenceError( + `${map.inputPath} contains a circular reference (using collections) to its own templateContent.`, + ); + } else { + // rethrow? + throw e; + } + } + } + } + + getTaggedCollection(tag) { + let result; + if (!tag || tag === "all") { + result = this.collection.getAllSorted(); + } else { + result = this.collection.getFilteredByTag(tag); + } + + // May not return an array (can be anything) + // https://www.11ty.dev/docs/collections-api/#return-values + debug(`Collection: collections.${tag || "all"} size: ${result?.length}`); + + return result; + } + + /* 3.0.0-alpha.1: setUserConfigCollections method removed (was only used for testing) */ + isUserConfigCollectionName(name) { + let collections = this.userConfig.getCollections(); + return name && !!collections[name]; + } + + getUserConfigCollectionNames() { + return Object.keys(this.userConfig.getCollections()); + } + + async getUserConfigCollection(name) { + let configCollections = this.userConfig.getCollections(); + + // This works with async now + let result = await configCollections[name](this.collection); + + // May not return an array (can be anything) + // https://www.11ty.dev/docs/collections-api/#return-values + debug(`Collection: collections.${name} size: ${result?.length}`); + return result; + } + + populateCollectionsWithContent() { + for (let collectionName in this.collectionsData) { + // skip custom collections set in configuration files that have arbitrary types + if (!Array.isArray(this.collectionsData[collectionName])) { + continue; + } + + for (let item of this.collectionsData[collectionName]) { + // skip custom collections set in configuration files that have arbitrary types + if (!isPlainObject(item) || !("inputPath" in item)) { + continue; + } + + let entry = this.getMapEntryForInputPath(item.inputPath); + // This check skips precompiled collections + if (entry) { + let index = item.pageNumber || 0; + let content = entry._pages[index]._templateContent; + if (content !== undefined) { + item.templateContent = content; + } + } + } + } + } + + async resolveRemainingComputedData() { + let promises = []; + for (let entry of this.map) { + for (let pageEntry of entry._pages) { + if (this.config.keys.computed in pageEntry.data) { + promises.push(pageEntry.template.resolveRemainingComputedData(pageEntry.data)); + } + } + } + return Promise.all(promises); + } + + async generateLayoutsMap() { + let layouts = {}; + + for (let entry of this.map) { + for (let page of entry._pages) { + let tmpl = page.template; + if (tmpl.templateUsesLayouts(page.data)) { + let layoutKey = page.data[this.config.keys.layout]; + let layout = tmpl.getLayout(layoutKey); + let layoutChain = await layout.getLayoutChain(); + let priors = []; + for (let filepath of layoutChain) { + if (!layouts[filepath]) { + layouts[filepath] = new Set(); + } + layouts[filepath].add(page.inputPath); + for (let prior of priors) { + layouts[filepath].add(prior); + } + priors.push(filepath); + } + } + } + } + + for (let key in layouts) { + layouts[key] = Array.from(layouts[key]); + } + + return layouts; + } + + #onEachPage(callback) { + for (let template of this.map) { + for (let page of template._pages) { + callback(page, template); + } + } + } + + checkForDuplicatePermalinks() { + let inputs = {}; + let outputPaths = {}; + let warnings = {}; + this.#onEachPage((page, template) => { + if (page.outputPath === false || page.url === false) { + // do nothing (also serverless) + } else { + // Make sure output doesn’t overwrite input (e.g. --input=. --output=.) + // Related to https://github.com/11ty/eleventy/issues/3327 + if (page.outputPath === page.inputPath) { + throw new DuplicatePermalinkOutputError( + `The template at "${page.inputPath}" attempted to overwrite itself.`, + ); + } else if (inputs[page.outputPath]) { + throw new DuplicatePermalinkOutputError( + `The template at "${page.inputPath}" attempted to overwrite an existing template at "${page.outputPath}".`, + ); + } + inputs[page.inputPath] = true; + + if (!outputPaths[page.outputPath]) { + outputPaths[page.outputPath] = [template.inputPath]; + } else { + warnings[page.outputPath] = `Output conflict: multiple input files are writing to \`${ + page.outputPath + }\`. Use distinct \`permalink\` values to resolve this conflict. + 1. ${template.inputPath} +${outputPaths[page.outputPath] + .map(function (inputPath, index) { + return ` ${index + 2}. ${inputPath}\n`; + }) + .join("")} +`; + outputPaths[page.outputPath].push(template.inputPath); + } + } + }); + + let warningList = Object.values(warnings); + if (warningList.length) { + // throw one at a time + throw new DuplicatePermalinkOutputError(warningList[0]); + } + } + + checkForMissingFileExtensions() { + // disabled in config + if (this.userConfig?.errorReporting?.allowMissingExtensions === true) { + return; + } + + this.#onEachPage((page) => { + if ( + page.outputPath === false || + page.url === false || + page.data.eleventyAllowMissingExtension || + EXTENSIONLESS_URL_ALLOWLIST.some((url) => page.url.endsWith(url)) + ) { + // do nothing (also serverless) + } else { + if (TemplatePath.getExtension(page.outputPath) === "") { + let e = + new Error(`The template at '${page.inputPath}' attempted to write to '${page.outputPath}'${page.data.permalink ? ` (via \`permalink\` value: '${page.data.permalink}')` : ""}, which is a target on the file system that does not include a file extension. + +You *probably* want to add a file extension to your permalink so that hosts will know how to correctly serve this file to web browsers. Without a file extension, this file may not be reliably deployed without additional hosting configuration (it won’t have a mime type) and may also cause local development issues if you later attempt to write to a subdirectory of the same name. + +Learn more: https://v3.11ty.dev/docs/permalinks/#trailing-slashes + +This is usually but not *always* an error so if you’d like to disable this error message, add \`eleventyAllowMissingExtension: true\` somewhere in the data cascade for this template or use \`eleventyConfig.configureErrorReporting({ allowMissingExtensions: true });\` to disable this feature globally.`); + e.skipOriginalStack = true; + throw e; + } + } + }); + } + + // TODO move these into TemplateMapTest.js + _testGetAllTags() { + let allTags = {}; + for (let map of this.map) { + let tags = map.data.tags; + if (Array.isArray(tags)) { + for (let tag of tags) { + allTags[tag] = true; + } + } + } + return Object.keys(allTags); + } + + async _testGetUserConfigCollectionsData() { + let collections = {}; + let configCollections = this.userConfig.getCollections(); + + for (let name in configCollections) { + collections[name] = configCollections[name](this.collection); + + debug(`Collection: collections.${name} size: ${collections[name].length}`); + } + + return collections; + } + + async _testGetTaggedCollectionsData() { + let collections = {}; + collections.all = this.collection.getAllSorted(); + debug(`Collection: collections.all size: ${collections.all.length}`); + + let tags = this._testGetAllTags(); + for (let tag of tags) { + collections[tag] = this.collection.getFilteredByTag(tag); + debug(`Collection: collections.${tag} size: ${collections[tag].length}`); + } + return collections; + } + + async _testGetAllCollectionsData() { + let collections = {}; + let taggedCollections = await this._testGetTaggedCollectionsData(); + Object.assign(collections, taggedCollections); + + let userConfigCollections = await this._testGetUserConfigCollectionsData(); + Object.assign(collections, userConfigCollections); + + return collections; + } + + async _testGetCollectionsData() { + if (!this.cached) { + await this.cache(); + } + + return this.collectionsData; + } +} + +export default TemplateMap; -- cgit v1.2.3