// Copyright 2023 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ============================================================================= import * as fs from 'fs'; import * as path from 'path'; export const DEPENDENCY_GRAPH = JSON.parse( fs.readFileSync(path.join(__dirname, 'package_dependencies.json'), 'utf8')); // This is a reverse dependencies graph. Each entry in the graph lists the // packages that depend on it. export const REVERSE_DEPENDENCY_GRAPH = transposeGraph(DEPENDENCY_GRAPH); // Topologically sort the dependency tree and arrange // steps in dependency order. export const DEPENDENCY_ORDER = topologicalSort(DEPENDENCY_GRAPH); export type Graph = Iterable> = { [node: string]: V } /** * Verify that an object is a valid graph. */ export function verifyGraph(graph: Graph) { const nodes = new Set(Object.keys(graph)); for (const [node, edges] of Object.entries(graph)) { for (const edge of edges) { if (!nodes.has(edge)) { throw new Error( `Graph edge ${edge} of node ${node} not found in the graph`); } } } } /** * Transpose a directed graph i.e. reverse the direction of the edges. */ export function transposeGraph(graph: Graph) { verifyGraph(graph); const transposed: Graph> = {}; for (const [nodeName, connectedNodes] of Object.entries(graph)) { for (const connectedNode of connectedNodes) { if (!transposed[connectedNode]) { transposed[connectedNode] = new Set(); } if (!transposed[nodeName]) { // Make sure the node itself ends up in the transposed graph. transposed[nodeName] = new Set(); } transposed[connectedNode].add(nodeName); } } return transposed; } /** * Topologically sort a directed acyclic graph. * * Returns a list of graph nodes such that, by following edges, * you can only move forward in the list, not backward. */ export function topologicalSort(graph: Graph) { // We can't use a standard sorting algorithm because // often, two packages won't have any dependency relationship // between each other, meaning they are incomparable. verifyGraph(graph); const sorted: string[] = []; while (sorted.length < Object.keys(graph).length) { // Find nodes not yet in 'sorted' that have edges // only to nodes already in 'sorted' const emptyNodes = Object.entries(graph) .filter(([node, edges]) => { if (sorted.includes(node)) { return false; } for (const edge of edges) { if (!sorted.includes(edge)) { return false; } } return true; }) .map(([node, edges]) => node); // If there are no such nodes, then the graph has a cycle. if (emptyNodes.length === 0) { throw new Error('Dependency graph has a cycle.'); } for (let node of emptyNodes) { sorted.push(node); } } return sorted; } /** * Find all subnodes in the subgraph generated by taking the transitive * closure at `node`. */ export function findSubgraph(node: string, graph: Graph, subnodes = new Set()) { const directSubnodes = graph[node]; if (directSubnodes) { for (const directSubnode of directSubnodes) { if (!subnodes.has(directSubnode)) { subnodes.add(directSubnode); findSubgraph(directSubnode, graph, subnodes); } } } return subnodes; } /** * Find the transitive closure of dependencies of the given packages. */ export function findDeps(packages: Iterable): Set { return new Set( [...packages] .map(packageName => findSubgraph(packageName, DEPENDENCY_GRAPH)) .reduce((a, b) => [...a, ...b], [])); } /** * Find the reverse dependencies of the given packages, i.e. find the * set of packages that include at least one of the given packages in * their transitive closure of dependencies. */ export function findReverseDeps(packages: Iterable): Set { return new Set( [...packages] .map(packageName => findSubgraph(packageName, REVERSE_DEPENDENCY_GRAPH)) .reduce((a, b) => [...a, ...b], [])); }