import { DepGraph as DependencyGraph } from "dependency-graph"; import debugUtil from "debug"; const debug = debugUtil("Eleventy:TemplateDepGraph"); const COLLECTION_PREFIX = "__collection:"; export class TemplateDepGraph extends DependencyGraph { static STAGES = ["[basic]", "[userconfig]", "[keys]", "all"]; #configCollectionNames = new Set(); constructor() { // BREAKING TODO move this back to non-circular with errors super({ circular: true }); let previous; // establish stage relationships, all uses keys, keys uses userconfig, userconfig uses tags for (let stageName of TemplateDepGraph.STAGES.filter(Boolean).reverse()) { let stageKey = `${COLLECTION_PREFIX}${stageName}`; if (previous) { this.uses(previous, stageKey); } previous = stageKey; } } uses(from, to) { this.addDependency(from, to); } addTag(tagName, type) { if ( tagName === "all" || (tagName.startsWith("[") && tagName.endsWith("]")) || this.#configCollectionNames.has(tagName) ) { return; } if (!type) { throw new Error( `Missing tag type for addTag. Expecting one of ${TemplateDepGraph.STAGES.map((entry) => entry.slice(1, -1)).join(" or ")}. Received: ${type}`, ); } debug("collection type %o uses tag %o", tagName, type); this.uses(`${COLLECTION_PREFIX}[${type}]`, `${COLLECTION_PREFIX}${tagName}`); } addConfigCollectionName(collectionName) { if (collectionName === "all") { return; } this.#configCollectionNames.add(collectionName); // Collection relationships to `[userconfig]` are added last, in unfilteredOrder() } cleanupCollectionNames(collectionNames = []) { let s = new Set(collectionNames); if (s.has("[userconfig]")) { return collectionNames; } let hasAnyConfigCollections = collectionNames.find((name) => { if (this.#configCollectionNames.has(name)) { return true; } return false; }); if (hasAnyConfigCollections) { s.add("[userconfig]"); } return Array.from(s); } addTemplate(filePath, consumes = [], publishesTo = []) { // Move to the beginning if it doesn’t consume anything if (consumes.length === 0) { this.uses(`${COLLECTION_PREFIX}[basic]`, filePath); } consumes = this.cleanupCollectionNames(consumes); publishesTo = this.cleanupCollectionNames(publishesTo); // Can’t consume AND publish to `all` simultaneously let consumesAll = consumes.includes("all"); if (consumesAll) { publishesTo = publishesTo.filter((entry) => entry !== "all"); } debug("%o consumes %o and publishes to %o", filePath, consumes, publishesTo); for (let collectionName of publishesTo) { if (!consumesAll) { let tagType = "basic"; let consumesUserConfigCollection = consumes.includes("[userconfig]"); if (consumesUserConfigCollection) { // must finish before [keys] tagType = "keys"; } this.addTag(collectionName, tagType); } this.uses(`${COLLECTION_PREFIX}${collectionName}`, filePath); } for (let collectionName of consumes) { this.uses(filePath, `${COLLECTION_PREFIX}${collectionName}`); let stageIndex = TemplateDepGraph.STAGES.indexOf(collectionName); let nextStage = stageIndex > 0 ? TemplateDepGraph.STAGES[stageIndex + 1] : undefined; if (nextStage) { this.uses(`${COLLECTION_PREFIX}${nextStage}`, filePath); } } } addDependency(from, to) { if (!this.hasNode(from)) { this.addNode(from); } if (!this.hasNode(to)) { this.addNode(to); } super.addDependency(from, to); } unfilteredOrder() { // these need to be added last, after the template map has been added (see addConfigCollectionName) for (let collectionName of this.#configCollectionNames) { this.uses(`${COLLECTION_PREFIX}[keys]`, `${COLLECTION_PREFIX}${collectionName}`); } return super.overallOrder(); } overallOrder() { let unfiltered = this.unfilteredOrder(); let filtered = unfiltered.filter((entry) => { if (entry === `${COLLECTION_PREFIX}[keys]`) { return true; } return !entry.startsWith(`${COLLECTION_PREFIX}[`) && !entry.endsWith("]"); }); let allKey = `${COLLECTION_PREFIX}all`; // Add another collections.all entry to the end (if not already the last one) if (filtered[filtered.length - 1] !== allKey) { filtered.push(allKey); } return filtered; } }