Last active
July 27, 2026 15:12
-
-
Save LouDnl/d9cbe3669be40f89d3b62555b462573d to your computer and use it in GitHub Desktop.
Namespace require tree in require order, analogous to `clj -X:deps tree` but for ns requires
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bb | |
| ;; 2026 LouD https://github.com/LouDnl | |
| ;; Source: https://gist.github.com/LouDnl/d9cbe3669be40f89d3b62555b462573d | |
| ;; Namespace require tree in require order, analogous to `clj -X:deps tree` but for ns requires. | |
| ;; | |
| ;; Command (uses glow to view the markdown file afterwards): | |
| ;; bb ns-tree.bb yourns.core --order source --output path/to/yourns-tree.md && glow path/to/yourns-tree.md -altw0 | |
| ;; bb ns-tree.bb yourns.core --order source > path/to/yourns-tree.md && glow path/to/yourns-tree.md -altw0 | |
| ;; Usage: | |
| ;; bb ns-tree.bb <root-ns> [opts] | |
| ;; bb ns-tree.bb yourns.core | |
| ;; bb ns-tree.bb yourns.core --prefix yourns ; only show nses under this prefix (default: all) | |
| ;; bb ns-tree.bb --list ; list all namespaces found | |
| ;; bb ns-tree.bb yourns.core --paths src:test ; analysis paths (default: src) | |
| ;; bb ns-tree.bb yourns.core --order source ; keep :require order (default: alpha) | |
| ;; bb ns-tree.bb yourns.core --cache ; persist analysis cache (default: none left behind) | |
| ;; | |
| ;; Default runs a fresh clj-kondo analysis and leaves no cache file. | |
| ;; --cache persists .ns-tree-cache.edn and auto-refreshes it when any source is newer. | |
| (require | |
| '[clojure.java.shell :refer [sh]] | |
| '[clojure.string :as string] | |
| '[clojure.edn :as edn] | |
| '[babashka.fs :as fs]) | |
| (def ^:dynamic *prefix* nil) | |
| (defn parse-opts [args] | |
| (loop [a args, m {:paths "src"}] | |
| (if-let [x (first a)] | |
| (cond | |
| (= x "--list") (recur (rest a) (assoc m :list true)) | |
| (= x "--prefix") (recur (drop 2 a) (assoc m :prefix (second a))) | |
| (= x "--paths") (recur (drop 2 a) (assoc m :paths (second a))) | |
| (= x "--order") (recur (drop 2 a) (assoc m :order (keyword (second a)))) | |
| (= x "--cache") (recur (rest a) (assoc m :cache true)) | |
| (= x "--output") (recur (drop 2 a) (assoc m :output (second a))) | |
| (string/starts-with? x "--") (recur (rest a) m) | |
| :else (recur (rest a) (assoc m :root x))) | |
| m))) | |
| (defn run-analysis | |
| [paths] | |
| (let [cfg "{:output {:analysis {:namespace-definitions true :namespace-usages true} :format :edn} :analysis true}" | |
| {:keys [out exit err]} (sh "clj-kondo" "--lint" paths "--config" cfg)] | |
| (when (and (string/blank? out) (not (zero? exit))) | |
| (binding [*out* *err*] (println "clj-kondo failed:" err)) | |
| (System/exit 1)) | |
| (:analysis (edn/read-string out)))) | |
| (defn newest-source-ms | |
| [paths] | |
| (->> (string/split paths #":") | |
| (filter fs/exists?) | |
| (mapcat #(fs/glob % "**.{clj,cljc,cljs,edn}")) | |
| (map #(fs/file-time->millis (fs/last-modified-time %))) | |
| (reduce max 0))) | |
| (defn cache-fresh? | |
| [cache paths] | |
| (and | |
| (fs/exists? cache) | |
| (> (fs/file-time->millis (fs/last-modified-time cache)) | |
| (newest-source-ms paths)))) | |
| (def cache-file (fs/file ".ns-tree-cache.edn")) | |
| ;; Default: run fresh, no persisted cache (avoids silent staleness). | |
| ;; --cache: persist analysis, auto-refreshed when any source is newer than cache. | |
| (defn load-analysis | |
| [{:keys [paths cache]}] | |
| (if (and cache (cache-fresh? cache-file paths)) | |
| (edn/read-string (slurp (str cache-file))) | |
| (let [a (run-analysis paths)] | |
| (when cache (spit (str cache-file) (pr-str a))) | |
| a))) | |
| ;; Remove any stale/leftover cache unless the caller opted into persistence. | |
| (defn cleanup-cache! | |
| [{:keys [cache]}] | |
| (when (and (not cache) (fs/exists? cache-file)) | |
| (fs/delete cache-file))) | |
| ;; adjacency: from-ns -> ordered, deduped vec of to-ns | |
| ;; order = :alpha (sort by name) | :source (sort by :row/:col in the from file) | |
| (defn build-graph | |
| [analysis order] | |
| (let [grouped (->> (:namespace-usages analysis) | |
| (reduce (fn [m {:keys [from to row col]}] | |
| (update m from (fnil conj []) | |
| {:to to :row (or row 0) :col (or col 0)})) | |
| {})) | |
| sortkey (if (= order :source) | |
| (juxt :row :col) | |
| (comp str :to))] | |
| (reduce-kv (fn [m from usages] | |
| (assoc m from (->> usages | |
| (sort-by sortkey) | |
| (map :to) | |
| (distinct) | |
| vec))) | |
| {} grouped))) | |
| (defn all-nses | |
| [analysis] | |
| (into (sorted-set) | |
| (concat (map :name (:namespace-definitions analysis)) | |
| (mapcat (juxt :from :to) (:namespace-usages analysis))))) | |
| (defn under-prefix? | |
| [ns] | |
| (or (nil? *prefix*) | |
| (string/starts-with? (str ns) (str *prefix*)))) | |
| (defn print-tree | |
| [graph root] | |
| (println root) | |
| (let [seen (atom #{})] | |
| (letfn [(walk [ns prefix] | |
| (let [kids (->> (get graph ns) (filter under-prefix?) vec) | |
| n (count kids)] | |
| (doseq-idx kids | |
| (fn [i child] | |
| (let [last? (= i (dec n)) | |
| branch (if last? "└── " "├── ") | |
| childpfx (str prefix (if last? " " "│ ")) | |
| cyc? (contains? @seen child)] | |
| (println (str prefix branch child | |
| (when cyc? " *"))) | |
| (when-not cyc? | |
| (swap! seen conj child) | |
| (walk child childpfx))))))) | |
| (doseq-idx [coll f] | |
| (dorun (map-indexed f coll)))] | |
| (swap! seen conj root) | |
| (walk root "")) | |
| (println) | |
| (println "* = already shown above (or cycle); subtree omitted"))) | |
| (defn today-str | |
| ; Current date as YYYY-MM-DD | |
| [] | |
| (.format | |
| (java.time.LocalDate/now) | |
| (java.time.format.DateTimeFormatter/ofPattern "dd-MM-yyyy"))) | |
| (def base-output-header | |
| "### Current %s tree output (updated %s) | |
| Re-generate with | |
| ```shell | |
| bb ns-tree.bb %s --order source --output %s | |
| ``` | |
| ### Output | |
| ") | |
| (defn print-to-output | |
| [output graph root] | |
| (let [header (format base-output-header root (today-str) root output) | |
| tree (with-out-str (print-tree graph root))] | |
| (spit output header) | |
| (spit output tree :append true))) | |
| (let [{:keys [root list prefix order output] :as opts} (parse-opts *command-line-args*) | |
| analysis (load-analysis opts)] | |
| (binding [*prefix* prefix] | |
| (cond | |
| list (doseq [n (filter under-prefix? (all-nses analysis))] (println n)) | |
| (nil? root) (println "usage: bb ns-tree.bb <root-ns> [--prefix P] [--paths src:test] [--order alpha|source] [--cache] [--list] [--output path/to/file.md]") | |
| output (print-to-output output (build-graph analysis (or order :alpha)) (symbol root)) | |
| :else (print-tree (build-graph analysis (or order :alpha)) (symbol root)))) | |
| (cleanup-cache! opts)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment