Path: blob/main/package/src/common/import-report/explain-all-cycles.ts
6452 views
import { resolve, toFileUrl } from "../../../../src/deno_ral/path.ts";1import {2DependencyGraph,3Edge,4getDenoInfo,5graphTranspose,6moduleGraph,7reachability,8} from "./deno-info.ts";910import { longestCommonDirPrefix } from "./utils.ts";1112function dropTypesFiles(edges: Edge[])13{14return edges.filter(({15"from": edgeFrom,16to,17}) => !(to.endsWith("types.ts") || edgeFrom.endsWith("types.ts")));18}1920function trimCommonPrefix(edges: Edge[])21{22// https://stackoverflow.com/a/6870296623const strs: Set<string> = new Set();24for (const { "from": edgeFrom, to } of edges) {25strs.add(edgeFrom);26strs.add(to);27}28const p = longestCommonDirPrefix(Array.from(strs)).length;29return edges.map(({ "from": edgeFrom, to }) => ({30"from": edgeFrom.slice(p),31to: to.slice(p),32}));33}3435function simplify(36edges: Edge[],37prefixes: string[],38): Edge[]39{40edges = trimCommonPrefix(edges);4142const result: Edge[] = [];43const keepPrefix = (s: string) => {44for (const prefix of prefixes) {45if (s.startsWith(prefix)) {46return prefix + "...";47}48}49return s;50}51const edgeSet = new Set<string>();52for (const edge of edges) {53const from = keepPrefix(edge.from);54const to = keepPrefix(edge.to);55if (from === to) {56continue;57}58const key = `${from} -> ${to}`;59if (edgeSet.has(key)) {60continue;61}62edgeSet.add(key);63result.push({ from, to });64}65return result;66}6768function explain(69graph: DependencyGraph,70): Edge[] {71const result: Edge[] = [];72const transpose = graphTranspose(graph);73const visited: Set<string> = new Set();74let found = false;75let count = 0;7677const deps = reachability(graph);7879for (const source of Object.keys(graph)) {80if (!deps[source]) {81continue;82}83found = true;84const inner = (node: string) => {85if (visited.has(node)) {86return;87}88visited.add(node);89for (const pred of transpose[node]) {90if (deps[pred].has(source) || pred === source) {91result.push({ "from": pred, "to": node });92inner(pred);93}94}95};96inner(source);97}98if (!found) {99console.error(`graph does not have cyclic imports`);100Deno.exit(1);101}102return result;103}104105function generateGraph(edges: Edge[]): string {106const qmdOut: string[] = [];107qmdOut.push("digraph G {");108const m: Record<string, number> = {};109let ix = 1;110for (const { "from": edgeFrom, to } of edges) {111if (m[edgeFrom] === undefined) {112m[edgeFrom] = ix++;113}114if (m[to] === undefined) {115m[to] = ix++;116}117qmdOut.push(` ${m[edgeFrom]} -> ${m[to]};`);118}119for (const [name, ix] of Object.entries(m)) {120qmdOut.push(` ${ix} [ label = "${name}", shape = "none" ];`);121}122qmdOut.push("}\n");123return qmdOut.join("");124}125126async function buildOutput(edges: Edge[]) {127const qmdOut: string[] = [`---128title: explain.ts129format: html130---131`];132qmdOut.push(133"This graph shows all cyclic import chains.\n\n",134);135qmdOut.push("```{ojs}\n//| echo: false\n\ndot`");136qmdOut.push(generateGraph(edges));137qmdOut.push("`\n");138qmdOut.push("```\n");139140const filename = await Deno.makeTempFile({ suffix: ".qmd" });141Deno.writeTextFileSync(filename, qmdOut.join("\n"));142143const process = Deno.run({144cmd: ["quarto", "preview", filename],145});146await process.status();147148Deno.remove(filename);149}150151if (import.meta.main) {152if (Deno.args.length === 0) {153console.log(154`explain-all-cycles.ts: generate a graph visualization of cyclic imports155between all files reachable from some source file.156157Usage:158$ quarto run --dev explain-all-cycles.ts <entry-point.ts> [--simplify <prefixes...>] [--graph|--toon [filename]]159160Options:161--simplify <prefixes...> Collapse paths with given prefixes (must be first if used)162--graph [filename] Output .dot specification (default: graph.dot)163--toon [filename] Output edges in TOON format (default: cycles.toon)164165Examples:166$ quarto run --dev package/src/common/import-report/explain-all-cycles.ts src/quarto.ts167$ quarto run --dev package/src/common/import-report/explain-all-cycles.ts src/quarto.ts --simplify core/ command/ --toon168$ quarto run --dev package/src/common/import-report/explain-all-cycles.ts src/quarto.ts --graph cycles.dot169170If no output option is given, opens an interactive preview.171`,172);173Deno.exit(1);174}175const json = await getDenoInfo(Deno.args[0]);176const { graph } = moduleGraph(json);177178let args = Array.from(Deno.args);179180let result = explain(graph);181if (args[1] === "--simplify") {182args.splice(1, 1);183const prefixes = [];184while (!args[1].startsWith("--")) {185prefixes.push(args[1]);186args.splice(1, 1);187}188result = simplify(result, prefixes);189}190191result = dropTypesFiles(result);192193if (args[1] === "--toon") {194// Output in TOON format195const lines = [`edges[${result.length}]{from,to}:`];196for (const { from, to } of result) {197lines.push(` ${from},${to}`);198}199Deno.writeTextFileSync(200args[2] ?? "cycles.toon",201lines.join("\n") + "\n",202);203} else if (args[1] === "--graph") {204Deno.writeTextFileSync(205args[2] ?? "graph.dot",206generateGraph(result),207);208} else {209await buildOutput(result);210}211}212213214