clojure.tools.namespace is a library that helps you track changes in your project’s filesystem and namespace dependencies. In that is clojure.tools.namespace.dir, which has a scan-dirs function that accepts a map and populates it with the current projects files and dependencies.

=> (d/scan-dirs {})
{:clojure.tools.namespace.dir/files
 #{#object[java.io.File 0x56b45f48 "/Projects/my-project/src/my_project/core.clj"]
   #object[java.io.File 0x26c1717 "/Projects/my-project/spec/my_project/core_spec.clj"]},
 :clojure.tools.namespace.track/deps
 {:dependencies
  {my-project.core-spec #{my-project.core speclj.core},
   my-project.core #{clojure.tools.namespace.dir clojure.pprint}},
  :dependents
  {my-project.core #{my-project.core-spec},
   speclj.core #{my-project.core-spec},
   clojure.tools.namespace.dir #{my-project.core},
   clojure.pprint #{my-project.core}}},
 :clojure.tools.namespace.track/unload
 (my-project.core-spec my-project.core),
 :clojure.tools.namespace.track/load
 (my-project.core my-project.core-spec),
 :clojure.tools.namespace.file/filemap
 {#object[java.io.File 0x26c1717 "/Projects/my-project/spec/my_project/core_spec.clj"]
  my-project.core-spec,
  #object[java.io.File 0x56b45f48 "/Projects/my-project/src/my_project/core.clj"]
  my-project.core},
 :clojure.tools.namespace.dir/time 1640969282001}

Okay so that’s a LOT of information! What do we actually care about here? Well let’s take a look at the keys to break this down a little.

=> (keys (d/scan-dirs {}))
(:clojure.tools.namespace.dir/files
 :clojure.tools.namespace.track/deps
 :clojure.tools.namespace.track/unload
 :clojure.tools.namespace.track/load
 :clojure.tools.namespace.file/filemap
 :clojure.tools.namespace.dir/time)

::files

The first thing we have here is files. This is basically just a listing of all Clojure files in a project.

:clojure.tools.namespace.dir/files
 #{#object[java.io.File 0x56b45f48 "/Projects/my-project/src/my_project/core.clj"]
   #object[java.io.File 0x26c1717 "/Projects/my-project/spec/my_project/core_spec.clj"]}

Now if we add new_cljs_file.cljs and new_cljc_file.cljc to src, we can rerun the scan and see that only the cljc file is added while the cljs file is ignored. I’m not exactly sure why this is–maybe there’s an option that can be passed down to support this? Or maybe this is just how scan-dirs is expected to operate.

:clojure.tools.namespace.dir/files
 #{#object[java.io.File 0xbdecc21 "/Projects/my-project/src/my_project/core.clj"]
   #object[java.io.File 0x4d33940d "/Projects/my-project/src/my_project/new_cljc_file.cljc"]
   #object[java.io.File 0x43cf6ea3 "/Projects/my-project/spec/my_project/core_spec.clj"]},

::deps

In our results, we also get a mapping of sources to dependencies as well as dependencies to sources.

:clojure.tools.namespace.track/deps
{:dependencies
 {my-project.core-spec #{my-project.core speclj.core},
  my-project.core #{clojure.tools.namespace.dir clojure.pprint}},
 :dependents
 {my-project.core #{my-project.core-spec},
  speclj.core #{my-project.core-spec},
  clojure.tools.namespace.dir #{my-project.core},
  clojure.pprint #{my-project.core}}}

:dependencies is a map where the keys represent a file namespace and the values represent what those namespaces depend on. So my-project.core, for example, :requires clojure.pprint and clojure.tools.namespace.dir. So in the :dependencies map, both of those namespaces are included for core.

Likewise, my-project.core-spec only :requires depends on my-project.core and speclj.core. So those namespaces are included in that namespace’s dependencies.

; my_project.clj
(ns my-project.core
    (:require [clojure.pprint :as pp]
              [clojure.tools.namespace.dir :as d]))

; my_project_spec.clj
(ns my-project.core-spec
    (:require [speclj.core :refer :all]
              [my-project.core :refer :all]))

On the other side of things, we have :dependents which is an inversion of this dependency map where the :required modules are mapped to those namespaces that depend on them.

::load & ::unload

These two keys don’t seem to change when I add, remove, or update files. So I can’t say with confidence what these actually represent. Although, they do seem to only hold the namespaces declared in my project’s source files.

:clojure.tools.namespace.track/unload
 (my-project.core-spec my-project.core),
 :clojure.tools.namespace.track/load
 (my-project.core my-project.core-spec)

::filemap

filemap is a map with java.io.File objects being the keys and the namespaces associated in those files as the values. Since each file should have a unique namespace, you should be able to look up a file by its namespace and a namespace by its file using the filemap.

:clojure.tools.namespace.file/filemap
 {#object[java.io.File 0x26c1717 "/Projects/my-project/spec/my_project/core_spec.clj"]
  my-project.core-spec,
  #object[java.io.File 0x56b45f48 "/Projects/my-project/src/my_project/core.clj"]
  my-project.core}

Something I found a bit confusing about this interface is that the input you provide scan-dirs doesn’t actually affect the output in any meaningful way. You don’t have a difference report, a new file report, or anything else like that. It really just gives you a listing of all the files and namespaces in your project.

Now that’s not to say that this function isn’t useful. scan-dirs gives you tons of useful information that you can work off of! Particularly ::filemap and ::deps. Without this, you’d have to write your own reader function, and who wants to do that?!