309 lines
8.2 KiB
JavaScript
309 lines
8.2 KiB
JavaScript
|
const { DepGraph } = require("dependency-graph");
|
|||
|
const { TemplatePath } = require("@11ty/eleventy-utils");
|
|||
|
const debug = require("debug")("Eleventy:Dependencies");
|
|||
|
|
|||
|
const PathNormalizer = require("./Util/PathNormalizer.js");
|
|||
|
|
|||
|
class GlobalDependencyMap {
|
|||
|
// dependency-graph requires these keys to be alphabetic strings
|
|||
|
static LAYOUT_KEY = "layout";
|
|||
|
static COLLECTION_PREFIX = "__collection:";
|
|||
|
|
|||
|
// URL object with a windows, with file:// already removed (from file:///C:/directory/ to /C:/directory/)
|
|||
|
static WINDOWS_DRIVE_URL_PATH = /^\/\w\:\//;
|
|||
|
|
|||
|
reset() {
|
|||
|
this._map = undefined;
|
|||
|
}
|
|||
|
|
|||
|
setConfig(config) {
|
|||
|
if (this.config) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
this.config = config;
|
|||
|
|
|||
|
// These have leading dot slashes, but so do the paths from Eleventy
|
|||
|
this.config.events.once("eleventy.layouts", (layouts) => {
|
|||
|
this.addLayoutsToMap(layouts);
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
removeAllLayoutNodes() {
|
|||
|
let nodes = this.map.overallOrder();
|
|||
|
for (let node of nodes) {
|
|||
|
let data = this.map.getNodeData(node);
|
|||
|
if (!data || !data.type || data.type !== GlobalDependencyMap.LAYOUT_KEY) {
|
|||
|
continue;
|
|||
|
}
|
|||
|
|
|||
|
this.map.removeNode(node);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Eleventy Layouts don’t show up in the dependency graph, so we handle those separately
|
|||
|
addLayoutsToMap(layouts) {
|
|||
|
// Clear out any previous layout relationships to make way for the new ones
|
|||
|
this.removeAllLayoutNodes();
|
|||
|
|
|||
|
for (let rawLayout in layouts) {
|
|||
|
let layout = this.normalizeNode(rawLayout);
|
|||
|
|
|||
|
// We add this pre-emptively to add the `layout` data
|
|||
|
if (!this.map.hasNode(layout)) {
|
|||
|
this.map.addNode(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 layouts[rawLayout]) {
|
|||
|
this.addDependency(pageTemplate, [layout]);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
get map() {
|
|||
|
if (!this._map) {
|
|||
|
this._map = new DepGraph({ circular: true });
|
|||
|
}
|
|||
|
|
|||
|
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();
|
|||
|
}
|
|||
|
|
|||
|
// Fix file:///Users/ or file:///C:/ paths passed in
|
|||
|
if (node.startsWith("file://")) {
|
|||
|
node = node.slice("file://".length);
|
|||
|
if (node.match(GlobalDependencyMap.WINDOWS_DRIVE_URL_PATH)) {
|
|||
|
node = node.slice(1); // take off the leading slash, /C:/ becomes C:/
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (typeof node !== "string") {
|
|||
|
throw new Error("`addDependencies` files must be strings. Received:" + node);
|
|||
|
}
|
|||
|
|
|||
|
// Paths should not be absolute (we convert absolute paths to relative)
|
|||
|
// Paths should not have a leading dot slash
|
|||
|
// Paths should always be `/` independent of OS path separator
|
|||
|
return TemplatePath.stripLeadingDotSlash(
|
|||
|
PathNormalizer.normalizeSeperator(TemplatePath.relativePath(node))
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
getDependantsFor(node) {
|
|||
|
if (!node) {
|
|||
|
return new Set();
|
|||
|
}
|
|||
|
|
|||
|
node = this.normalizeNode(node);
|
|||
|
|
|||
|
if (!this.map.hasNode(node)) {
|
|||
|
return new Set();
|
|||
|
}
|
|||
|
|
|||
|
// 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)
|
|||
|
.filter((entry) => {
|
|||
|
return entry.startsWith(GlobalDependencyMap.COLLECTION_PREFIX);
|
|||
|
})
|
|||
|
.map((entry) => {
|
|||
|
return GlobalDependencyMap.getEntryFromCollectionKey(entry);
|
|||
|
});
|
|||
|
|
|||
|
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 collectionName = GlobalDependencyMap.getCollectionKeyForEntry(name);
|
|||
|
if (!this.map.hasNode(collectionName)) {
|
|||
|
continue;
|
|||
|
}
|
|||
|
for (let node of this.map.dependantsOf(collectionName)) {
|
|||
|
if (!node.startsWith(GlobalDependencyMap.COLLECTION_PREFIX)) {
|
|||
|
templates.add(node);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return templates;
|
|||
|
}
|
|||
|
|
|||
|
// 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;
|
|||
|
}
|
|||
|
|
|||
|
return this.map.dependenciesOf(node).filter((node) => {
|
|||
|
if (includeLayouts) {
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
// When includeLayouts is `false` we want to filter out layouts
|
|||
|
let data = this.map.getNodeData(node);
|
|||
|
if (data && data.type && data.type === "layout") {
|
|||
|
return false;
|
|||
|
}
|
|||
|
return true;
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
// node arguments are already normalized
|
|||
|
_addDependency(from, toArray = []) {
|
|||
|
this.map.addNode(from);
|
|||
|
|
|||
|
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) {
|
|||
|
if (!this.map.hasNode(to)) {
|
|||
|
this.map.addNode(to);
|
|||
|
}
|
|||
|
if (from !== to) {
|
|||
|
this.map.addDependency(from, to);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
addDependency(from, toArray = []) {
|
|||
|
this._addDependency(
|
|||
|
this.normalizeNode(from),
|
|||
|
toArray.map((to) => this.normalizeNode(to))
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
static getEntryFromCollectionKey(entry) {
|
|||
|
return entry.slice(GlobalDependencyMap.COLLECTION_PREFIX.length);
|
|||
|
}
|
|||
|
|
|||
|
static getCollectionKeyForEntry(entry) {
|
|||
|
return `${GlobalDependencyMap.COLLECTION_PREFIX}${entry}`;
|
|||
|
}
|
|||
|
|
|||
|
addDependencyConsumesCollection(from, collectionName) {
|
|||
|
let nodeName = this.normalizeNode(from);
|
|||
|
debug("%o depends on collection: %o", nodeName, collectionName);
|
|||
|
this._addDependency(nodeName, [GlobalDependencyMap.getCollectionKeyForEntry(collectionName)]);
|
|||
|
}
|
|||
|
|
|||
|
addDependencyPublishesToCollection(from, collectionName) {
|
|||
|
let normalizedFrom = this.normalizeNode(from);
|
|||
|
this._addDependency(GlobalDependencyMap.getCollectionKeyForEntry(collectionName), [
|
|||
|
normalizedFrom,
|
|||
|
]);
|
|||
|
}
|
|||
|
|
|||
|
// 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
|
|||
|
if (this.hasDependency(fullTemplateInputPath, comparisonFile, includeLayouts)) {
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
stringify() {
|
|||
|
return JSON.stringify(this.map);
|
|||
|
}
|
|||
|
|
|||
|
restore(persisted) {
|
|||
|
let obj = JSON.parse(persisted);
|
|||
|
let graph = new DepGraph({ circular: true });
|
|||
|
|
|||
|
// https://github.com/jriecken/dependency-graph/issues/44
|
|||
|
for (let key in obj) {
|
|||
|
graph[key] = obj[key];
|
|||
|
}
|
|||
|
this.map = graph;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
module.exports = GlobalDependencyMap;
|