Skip to content

Instantly share code, notes, and snippets.

@LouDnl
Last active July 27, 2026 15:12
Show Gist options
  • Select an option

  • Save LouDnl/d9cbe3669be40f89d3b62555b462573d to your computer and use it in GitHub Desktop.

Select an option

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
#!/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