Files
firefrost-website/node_modules/@11ty/eleventy/src/GlobalDependencyMap.js
2026-04-02 18:39:00 -05:00

464 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import debugUtil from "debug";
import { TemplatePath } from "@11ty/eleventy-utils";
import JavaScriptDependencies from "./Util/JavaScriptDependencies.js";
import PathNormalizer from "./Util/PathNormalizer.js";
import { TemplateDepGraph } from "./Util/TemplateDepGraph.js";
const debug = debugUtil("Eleventy:Dependencies");
class GlobalDependencyMap {
// dependency-graph requires these keys to be alphabetic strings
static LAYOUT_KEY = "layout";
static COLLECTION_PREFIX = "__collection:"; // must match TemplateDepGraph key
#map;
#templateConfig;
#cachedUserConfigurationCollectionApiNames;
static isCollection(entry) {
return entry.startsWith(this.COLLECTION_PREFIX);
}
static getTagName(entry) {
if (this.isCollection(entry)) {
return entry.slice(this.COLLECTION_PREFIX.length);
}
}
static getCollectionKeyForEntry(entry) {
return `${GlobalDependencyMap.COLLECTION_PREFIX}${entry}`;
}
reset() {
this.#map = undefined;
}
setIsEsm(isEsm) {
this.isEsm = isEsm;
}
setTemplateConfig(templateConfig) {
this.#templateConfig = templateConfig;
// These have leading dot slashes, but so do the paths from Eleventy
this.#templateConfig.userConfig.events.once("eleventy.layouts", async (layouts) => {
await this.addLayoutsToMap(layouts);
});
}
get userConfigurationCollectionApiNames() {
if (this.#cachedUserConfigurationCollectionApiNames) {
return this.#cachedUserConfigurationCollectionApiNames;
}
return Object.keys(this.#templateConfig.userConfig.getCollections()) || [];
}
initializeUserConfigurationApiCollections() {
this.addCollectionApiNames(this.userConfigurationCollectionApiNames);
}
// For Testing
setCollectionApiNames(names = []) {
this.#cachedUserConfigurationCollectionApiNames = names;
}
addCollectionApiNames(names = []) {
if (!names || names.length === 0) {
return;
}
for (let collectionName of names) {
this.map.addConfigCollectionName(collectionName);
}
}
filterOutLayouts(nodes = []) {
return nodes.filter((node) => {
if (GlobalDependencyMap.isLayoutNode(this.map, node)) {
return false;
}
return true;
});
}
filterOutCollections(nodes = []) {
return nodes.filter((node) => !node.startsWith(GlobalDependencyMap.COLLECTION_PREFIX));
}
static removeLayoutNodes(graph, nodeList) {
return nodeList.filter((node) => {
if (this.isLayoutNode(graph, node)) {
return false;
}
return true;
});
}
removeLayoutNodes(normalizedLayouts) {
let nodes = this.map.overallOrder();
for (let node of nodes) {
if (!GlobalDependencyMap.isLayoutNode(this.map, node)) {
continue;
}
// previous layout is not in the new layout map (no templates are using it)
if (!normalizedLayouts[node]) {
this.map.removeNode(node);
}
// important: if the layout map changed to have different templates (but was not removed)
// this is already handled by `resetNode` called via TemplateMap
}
}
// Eleventy Layouts dont show up in the dependency graph, so we handle those separately
async addLayoutsToMap(layouts) {
let normalizedLayouts = this.normalizeLayoutsObject(layouts);
// Clear out any previous layout relationships to make way for the new ones
this.removeLayoutNodes(normalizedLayouts);
for (let layout in normalizedLayouts) {
// We add this pre-emptively to add the `layout` data
if (!this.map.hasNode(layout)) {
this.map.addNode(layout, {
type: GlobalDependencyMap.LAYOUT_KEY,
});
} else {
this.map.setNodeData(layout, {
type: GlobalDependencyMap.LAYOUT_KEY,
});
}
// Potential improvement: only add the first template in the chain for a template and manage any upstream layouts by their own relationships
for (let pageTemplate of normalizedLayouts[layout]) {
this.addDependency(pageTemplate, [layout]);
}
if (this.#templateConfig?.shouldSpiderJavaScriptDependencies()) {
let deps = await JavaScriptDependencies.getDependencies([layout], this.isEsm);
this.addDependency(layout, deps);
}
}
}
get map() {
if (!this.#map) {
// this.#map = new DepGraph({ circular: true });
this.#map = new TemplateDepGraph();
}
return this.#map;
}
set map(graph) {
this.#map = graph;
}
normalizeNode(node) {
if (!node) {
return;
}
// TODO tests for this
// Fix URL objects passed in (sass does this)
if (typeof node !== "string" && "toString" in node) {
node = node.toString();
}
if (typeof node !== "string") {
throw new Error("`addDependencies` files must be strings. Received:" + node);
}
return PathNormalizer.fullNormalization(node);
}
normalizeLayoutsObject(layouts) {
let o = {};
for (let rawLayout in layouts) {
let layout = this.normalizeNode(rawLayout);
o[layout] = layouts[rawLayout].map((entry) => this.normalizeNode(entry));
}
return o;
}
getDependantsFor(node) {
if (!node) {
return [];
}
node = this.normalizeNode(node);
if (!this.map.hasNode(node)) {
return [];
}
// Direct dependants and dependencies, both publish and consume from collections
return this.map.directDependantsOf(node);
}
hasNode(node) {
return this.map.hasNode(this.normalizeNode(node));
}
findCollectionsRemovedFrom(node, collectionNames) {
if (!this.hasNode(node)) {
return new Set();
}
let prevDeps = this.getDependantsFor(node)
.map((entry) => GlobalDependencyMap.getTagName(entry))
.filter(Boolean);
let prevDepsSet = new Set(prevDeps);
let deleted = new Set();
for (let dep of prevDepsSet) {
if (!collectionNames.has(dep)) {
deleted.add(dep);
}
}
return deleted;
}
resetNode(node) {
node = this.normalizeNode(node);
if (!this.map.hasNode(node)) {
return;
}
// We dont want to remove relationships that consume this, controlled by the upstream content
// for (let dep of this.map.directDependantsOf(node)) {
// this.map.removeDependency(dep, node);
// }
for (let dep of this.map.directDependenciesOf(node)) {
this.map.removeDependency(node, dep);
}
}
getTemplatesThatConsumeCollections(collectionNames) {
let templates = new Set();
for (let name of collectionNames) {
let collectionKey = GlobalDependencyMap.getCollectionKeyForEntry(name);
if (!this.map.hasNode(collectionKey)) {
continue;
}
for (let node of this.map.dependantsOf(collectionKey)) {
if (!node.startsWith(GlobalDependencyMap.COLLECTION_PREFIX)) {
if (!GlobalDependencyMap.isLayoutNode(this.map, node)) {
templates.add(node);
}
}
}
}
return templates;
}
static isLayoutNode(graph, node) {
if (!graph.hasNode(node)) {
return false;
}
return graph.getNodeData(node)?.type === GlobalDependencyMap.LAYOUT_KEY;
}
getLayoutsUsedBy(node) {
node = this.normalizeNode(node);
if (!this.map.hasNode(node)) {
return [];
}
let layouts = [];
// include self, if layout
if (GlobalDependencyMap.isLayoutNode(this.map, node)) {
layouts.push(node);
}
this.map.dependantsOf(node).forEach((node) => {
// we only want layouts
if (GlobalDependencyMap.isLayoutNode(this.map, node)) {
return layouts.push(node);
}
});
return layouts;
}
// In order
// Does not include original templatePaths (unless *they* are second-order relevant)
getTemplatesRelevantToTemplateList(templatePaths) {
let overallOrder = this.map.overallOrder();
overallOrder = this.filterOutLayouts(overallOrder);
overallOrder = this.filterOutCollections(overallOrder);
let relevantLookup = {};
for (let inputPath of templatePaths) {
inputPath = TemplatePath.stripLeadingDotSlash(inputPath);
let deps = this.getDependencies(inputPath, false);
if (Array.isArray(deps)) {
let paths = this.filterOutCollections(deps);
for (let node of paths) {
relevantLookup[node] = true;
}
}
}
return overallOrder.filter((node) => {
if (relevantLookup[node]) {
return true;
}
return false;
});
}
// Layouts are not relevant to compile cache and can be ignored
getDependencies(node, includeLayouts = true) {
node = this.normalizeNode(node);
// `false` means the Node was unknown
if (!this.map.hasNode(node)) {
return false;
}
if (includeLayouts) {
return this.map.dependenciesOf(node).filter(Boolean);
}
return GlobalDependencyMap.removeLayoutNodes(this.map, this.map.dependenciesOf(node));
}
#addNode(name) {
if (this.map.hasNode(name)) {
return;
}
this.map.addNode(name);
}
// node arguments are already normalized
#addDependency(from, toArray = []) {
this.#addNode(from); // only if not already added
if (!Array.isArray(toArray)) {
throw new Error("Second argument to `addDependency` must be an Array.");
}
// debug("%o depends on %o", from, toArray);
for (let to of toArray) {
this.#addNode(to); // only if not already added
if (from !== to) {
this.map.addDependency(from, to);
}
}
}
addDependency(from, toArray = []) {
this.#addDependency(
this.normalizeNode(from),
toArray.map((to) => this.normalizeNode(to)),
);
}
addNewNodeRelationships(from, consumes = [], publishes = []) {
consumes = consumes.filter(Boolean);
publishes = publishes.filter(Boolean);
debug("%o consumes %o and publishes to %o", from, consumes, publishes);
from = this.normalizeNode(from);
this.map.addTemplate(from, consumes, publishes);
}
// Layouts are not relevant to compile cache and can be ignored
hasDependency(from, to, includeLayouts) {
to = this.normalizeNode(to);
let deps = this.getDependencies(from, includeLayouts); // normalizes `from`
if (!deps) {
return false;
}
return deps.includes(to);
}
// Layouts are not relevant to compile cache and can be ignored
isFileRelevantTo(fullTemplateInputPath, comparisonFile, includeLayouts) {
fullTemplateInputPath = this.normalizeNode(fullTemplateInputPath);
comparisonFile = this.normalizeNode(comparisonFile);
// No watch/serve changed file
if (!comparisonFile) {
return false;
}
// The file that changed is the relevant file
if (fullTemplateInputPath === comparisonFile) {
return true;
}
// The file that changed is a dependency of the template
// comparisonFile is used by fullTemplateInputPath
if (this.hasDependency(fullTemplateInputPath, comparisonFile, includeLayouts)) {
return true;
}
return false;
}
isFileUsedBy(parent, child, includeLayouts) {
if (this.hasDependency(parent, child, includeLayouts)) {
// child is used by parent
return true;
}
return false;
}
getTemplateOrder() {
let order = [];
for (let entry of this.map.overallOrder()) {
order.push(entry);
}
return order;
}
stringify() {
return JSON.stringify(this.map, function replacer(key, value) {
// Serialize internal Map objects.
if (value instanceof Map) {
let obj = {};
for (let [k, v] of value) {
obj[k] = v;
}
return obj;
}
return value;
});
}
restore(persisted) {
let obj = JSON.parse(persisted);
let graph = new TemplateDepGraph();
// https://github.com/jriecken/dependency-graph/issues/44
// Restore top level serialized Map objects (in stringify above)
for (let key in obj) {
let map = graph[key];
for (let k in obj[key]) {
let v = obj[key][k];
map.set(k, v);
}
}
this.map = graph;
}
}
export default GlobalDependencyMap;