464 lines
11 KiB
JavaScript
464 lines
11 KiB
JavaScript
|
|
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 don’t 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 don’t 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;
|