Path: blob/main/package/src/common/import-report/explain-import-chain.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 explain(13graph: DependencyGraph,14source: string,15target: string,16): Edge[] {17const result: Edge[] = [];18const deps = reachability(graph, source);19if (!deps[target]) {20const [ss, st] = [source, target].map((s) =>21s.slice(longestCommonDirPrefix([source, target]).length)22);23console.error(`${ss} does not depend on ${st}`);24Deno.exit(1);25}26if (!deps[target].has(source)) {27return result;28}29const transpose = graphTranspose(graph);30const visited: Set<string> = new Set();31const inner = (node: string) => {32if (visited.has(node)) {33return;34}35visited.add(node);36for (const pred of transpose[node]) {37if (deps[pred].has(source) || pred === source) {38result.push({ "from": pred, "to": node });39inner(pred);40}41}42};43inner(target);44return result;45}4647function edgeColor(source: string, target: string) {48if (49source.indexOf("src/core/") !== -1 &&50target.indexOf("src/core/") === -151) {52return "red";53}54if (55source.indexOf("src/core/lib") !== -1 &&56target.indexOf("src/core/lib") === -157) {58return "red";59}60return undefined;61}6263function generateGraph(edges: Edge[], source: string, target: string): string {64// https://stackoverflow.com/a/6870296665const strs: Set<string> = new Set();66for (const { "from": edgeFrom, to } of edges) {67strs.add(edgeFrom);68strs.add(to);69}70const p = longestCommonDirPrefix(Array.from(strs)).length;7172const qmdOut: string[] = [];73qmdOut.push("digraph G {");74const m: Record<string, number> = {};75let ix = 1;76for (const { "from": edgeFrom, to } of edges) {77if (m[edgeFrom] === undefined) {78m[edgeFrom] = ix++;79}80if (m[to] === undefined) {81m[to] = ix++;82}8384const c = edgeColor(edgeFrom, to);85if (c) {86qmdOut.push(` ${m[edgeFrom]} -> ${m[to]} [color="${c}"];`);87} else {88qmdOut.push(` ${m[edgeFrom]} -> ${m[to]};`);89}90}91for (const [name, ix] of Object.entries(m)) {92if (name.slice(p) === source.slice(p)) {93qmdOut.push(94` ${ix} [ label = "${95name.slice(p)96}", shape = "box", fillcolor = "#1f77b4", style = "filled", fontcolor = "white" ];`,97);98} else if (name.slice(p) === target.slice(p)) {99qmdOut.push(100` ${ix} [ label = "${101name.slice(p)102}", shape = "box", fillcolor = "#ff7f0e", style = "filled" ];`,103);104} else {105qmdOut.push(` ${ix} [ label = "${name.slice(p)}", shape = "none" ];`);106}107}108qmdOut.push("}\n");109return qmdOut.join("");110}111112async function buildOutput(edges: Edge[], source: string, target: string) {113const strs: Set<string> = new Set();114for (const { "from": edgeFrom, to } of edges) {115strs.add(edgeFrom);116strs.add(to);117}118const p = longestCommonDirPrefix(Array.from(strs)).length;119120const qmdOut: string[] = [`---121title: explain.ts122format: html123---124`];125qmdOut.push(126"This graph shows all import chains from `" + source.slice(p) +127"` (a <span style='color:#1f77b4'>blue node</span>) to `" +128target.slice(p) +129"` (an <span style='color:#ff7f0e'>orange node</span>).\n\n",130);131qmdOut.push("```{ojs}\n//| echo: false\n\ndot`");132qmdOut.push(generateGraph(edges, source, target));133qmdOut.push("`\n");134qmdOut.push("```\n");135136const filename = await Deno.makeTempFile({ suffix: ".qmd" });137Deno.writeTextFileSync(filename, qmdOut.join("\n"));138139const process = Deno.run({140cmd: ["quarto", "preview", filename],141});142await process.status();143144Deno.remove(filename);145}146147if (import.meta.main) {148if (Deno.args.length === 0) {149console.log(150`explain-import-chain.ts: generate a graph visualization of import paths151between files in quarto.152153Usage:154$ quarto run --dev explain-import-chain.ts <source-file.ts> <target-file.ts> [--simplify] [--graph|--toon [filename]]155156Options:157--simplify Simplify paths by removing common prefix158--graph [file] Output .dot specification (default: import-chain.dot)159--toon [file] Output edges in TOON format (default: import-chain.toon)160161Examples:162163From project root:164165$ quarto run --dev package/src/common/import-report/explain-import-chain.ts src/command/render/render.ts src/core/esbuild.ts166$ quarto run --dev package/src/common/import-report/explain-import-chain.ts src/command/check/cmd.ts src/core/lib/external/regexpp.mjs167$ quarto run --dev package/src/common/import-report/explain-import-chain.ts src/command/render/render.ts src/core/esbuild.ts --simplify --toon168169If no dependencies exist, this script will report that:170171$ quarto run --dev package/src/common/import-report/explain-import-chain.ts package/src/bld.ts src/core/lib/external/tree-sitter-deno.js172173package/src/bld.ts does not depend on src/core/lib/external/tree-sitter-deno.js174175If no output option is given, opens an interactive preview.176`,177);178Deno.exit(1);179}180// Parse arguments181let simplify = false;182let args = Deno.args;183184if (args[2] === "--simplify") {185simplify = true;186args = [args[0], args[1], ...args.slice(3)];187}188189const json = await getDenoInfo(args[0]);190const { graph } = moduleGraph(json);191192const targetName = args[1];193const target = toFileUrl(resolve(targetName)).href;194let result = explain(graph, json.roots[0], target);195196// Apply simplification if requested197if (simplify && result.length > 0) {198const allPaths = result.map(e => [e.from, e.to]).flat();199const prefix = longestCommonDirPrefix(allPaths);200result = result.map(({ from, to }) => ({201from: from.slice(prefix.length),202to: to.slice(prefix.length),203}));204}205206if (args[2] === "--graph") {207Deno.writeTextFileSync(208args[3] || "import-chain.dot",209generateGraph(result, json.roots[0], target),210);211} else if (args[2] === "--toon") {212const lines = [`edges[${result.length}]{from,to}:`];213for (const { from, to } of result) {214lines.push(` ${from},${to}`);215}216Deno.writeTextFileSync(217args[3] || "import-chain.toon",218lines.join("\n") + "\n",219);220} else {221await buildOutput(result, json.roots[0], target);222}223}224225226