import { parseTlpdb } from "./parse_tlpdb.ts"; import { printDotForRoots, printTreeForest } from "./graph.ts"; import { parseArgs } from "jsr:@std/cli@1/parse-args"; const DEFAULT_URL = "https://mirror.ctan.org/systems/texlive/tlnet/tlpkg/texlive.tlpdb"; function usage() { console.log(`tlpdb-deps (Deno) Usage: deno run --allow-net --allow-read main.ts [options] [...] Options: --file= Read texlive.tlpdb from local file instead of fetching --url= Read texlive.tlpdb from URL (default: ${DEFAULT_URL}) --format=tree|dot Output format: tree (default) or dot --depth= Maximum recursion depth for tree (default: 0, 0 = unlimited) --reverse Show reverse-dependencies (packages that depend on the given ones) --minimal Print minimal explicit add list from given packages Notes: - Multiple arguments are treated as depth-0 roots and printed as a forest in tree mode. - With --minimal, outputs only those given packages that are not dependencies of any other given package. --help Show this help Examples: deno run --allow-net main.ts latex --format=tree deno run --allow-read main.ts --file=./texlive.tlpdb babel --format=dot > babel.dot `); } async function loadTlpdbText(opts: { file?: string; url?: string }) { if (opts.file) { return await Deno.readTextFile(opts.file); } else { const url = opts.url ?? DEFAULT_URL; const resp = await fetch(url); if (!resp.ok) { throw new Error( `failed to fetch ${url}: ${resp.status} ${resp.statusText}`, ); } return await resp.text(); } } async function main() { const rawArgs = Deno.args; const args = parseArgs(rawArgs, { string: ["file", "url", "format", "depth"], boolean: ["reverse", "help", "minimal"], default: { format: "tree", depth: "0", reverse: false }, }); if (args.help) { usage(); return; } const pkgs: string[] = args._.map(String); if (pkgs.length === 0) { console.error("Error: at least one package name must be given.\n"); usage(); Deno.exit(2); } const file = args.file as string | undefined; const url = args.url as string | undefined; const format = (args.format as string).toLowerCase(); let depth = parseInt(args.depth as string, 10); if (isNaN(depth) || depth < 0) depth = 10; const reverse = Boolean(args.reverse); const minimal = Boolean(args.minimal); try { const txt = await loadTlpdbText({ file, url }); const pkgsMap = parseTlpdb(txt); // Map // Build forward adjacency map (only package names that exist in tlpdb are included). const adjacencyFwd: Map = new Map(); for (const [name, entry] of pkgsMap.entries()) { adjacencyFwd.set(name, entry.depends ?? []); } // Determine which adjacency to use for graph printing let adjacency: Map = adjacencyFwd; if (reverse) { // Build reverse map const rev: Map = new Map(); for (const k of adjacencyFwd.keys()) rev.set(k, [] as string[]); for (const [p, deps] of adjacencyFwd.entries()) { for (const d of deps) { if (!rev.has(d)) rev.set(d, [] as string[]); rev.get(d)!.push(p); } } adjacency = rev; } // Validate requested packages const missing = pkgs.filter((p: string) => !pkgsMap.has(p)); if (missing.length > 0) { console.error("Warning: the following packages were not found in tlpdb:"); for (const m of missing) console.error(" - " + m); // continue anyway; nodes missing will be shown as leaf nodes (no deps known) } if (minimal) { // Compute minimal explicit add list from given packages. // If A depends on B and both are given, we can drop B. const given = Array.from(new Set(pkgs)); // Build reachability from each given root const reachableBy: Map> = new Map(); for (const g of given) { const seen = new Set(); collectReachable(g, adjacencyFwd, seen); reachableBy.set(g, seen); } // Keep only those not reachable from any other given root (excluding self) const keep: string[] = []; for (const g of given) { let isRedundant = false; for (const h of given) { if (h === g) continue; const seen = reachableBy.get(h)!; if (seen.has(g)) { isRedundant = true; break; } } if (!isRedundant) keep.push(g); } // Print one per line for easy scripting for (const k of keep.sort()) console.log(k); } else if (format === "dot") { console.log(printDotForRoots(adjacency, pkgs)); } else if (format === "tree") { // Treat multiple packages as depth-0 siblings printTreeForest(adjacency, pkgs, depth); } else { console.error("Unknown format: " + format); Deno.exit(2); } } catch (e) { console.error("Error:", e); Deno.exit(1); } } if (import.meta.main) { main(); } // Collect all nodes reachable from start (including start) function collectReachable( start: string, adjacency: Map, seen: Set, ) { if (seen.has(start)) return; seen.add(start); const deps = adjacency.get(start) ?? []; for (const d of deps) collectReachable(d, adjacency, seen); }