When I was first introduced to the JVM in college, it constantly
seemed like I was always having problems with the Classpath. Even
though these problems were constantly popping up I never really took
the time to understand it. Luckily when you are working Clojure and
ClojureScript you can get by with a simple mental model of the
Classpath for 99% of the work you do.
The primary purpose of Leiningen and CLI Tools is
to fetch the needed dependencies, and then invoke java with a
classpath to create an environment where these dependencies are
available. Tools like this make the classpath problems that I used to
experience mostly a thing of the past.
First let's take a look at a Classpath. As an example let's use the
project.clj or the deps.edn we created in the
Installation chapter.
Assuming you are in the root directory of your project using a
project.clj file as detailed in the last chapter. You can examine
the classpath with the following shell command:
Assuming you are in the root directory of your project which has a
deps.edn file as detailed in the last chapter. You can examine the
classpath with the following shell command:
If you look at the classpaths above you you will see a list of paths
separated by a : character. If you have ever looked at the $PATH
variable in your shell environment this should look familiar.
The classpath is a list of paths that will be used to search for
various artifacts. For instance Clojure uses the classpath to find and
load Clojure namespaces. ClojureScript does the same to find and
compile ClojureScript source files.
Another thing to notice about the classpaths above is that the
classpath starts with several local directories and then continues
with a quite of number of .jar files that represent our
dependencies.
If we format the classpath that CLI tools generated we
can see that it starts with a single local src directory.
$ clj -Spath | sed -e 's/:/\'$'\n/g' | head
src
/Users/bhauman/.m2/repository/com/cognitect/transit-java/0.8.332/transit-java-0.8.332.jar
/Users/bhauman/.m2/repository/org/clojure/data.json/0.2.6/data.json-0.2.6.jar
/Users/bhauman/.m2/repository/org/clojure/clojure/1.9.0/clojure-1.9.0.jar
/Users/bhauman/.m2/repository/joda-time/joda-time/2.8.2/joda-time-2.8.2.jar
/Users/bhauman/.m2/repository/commons-codec/commons-codec/1.10/commons-codec-1.10.jar
...
This local src directory is a default that is added to the classpath by
CLI Tools if no other paths are configured in the
deps.edn.
When the ClojureScript is looking for a namespace to analyze or
compile, it will utilize the classpath to find the file.
For example if a ClojureScript namespace hello-world.core is
required the ClojureScript compiler is going to utilize the classpath
to look for a file named hello_world/core.cljs. If our only local
path is src then hello_world/core.cljs is going to have to be in
the src directory in order to be found.
When you set up your project you will often have one or more source
directories. Some folks prefer to have a cljs-src directory that
only holds files relevant to a local ClojureScript codebase (i.e. a
front-end application).
Adding the cljs-src path with CLI Tools
Let's add the cljs-src path to our classpath with CLI Tools. In your
deps.edn file:
Above we added :paths key with the value ["src" "cljs-src"]. We have to
explicitly add "src" to the paths key because once it is defined the
src is no longer added implicitly.
Adding the cljs-src path with Leiningen
Let's add the cljs-src path to our classpath with CLI Tools. In your
project.clj file:
Above we added :source-paths key with the value ["src" "cljs-src"]. We have to
explicitly add "src" to the paths key because once it is defined the
src is no longer added implicitly.
Unlike CLI Tools, Leiningen has several keys to add paths to the
classpath depending on the type of path it is. This is because
Leiningen provides jar bundling built-in and needs to know which paths
to bundle. All of the following project.clj configuration keys add
paths to the classpath.
:source-paths - is for paths that hold source files
:resource-paths - holds paths to assets that you want
:target-paths - for paths that hold output files that can be safely deleted
We've been trying and learning different things so far. Let's apply
this to a project with a hello_world.core ClojureScript namespace
that looks like this:
Once we start to understand the classpath we can use it to our
advantage. We can bundle our web assets (HTML, CSS, Javascript files)
in a jar and easily access them from inside our web process.
A common pattern in Clojure web development is to serve our public web
assets from a public directory that is on the classpath. This is the
default for the Jetty server Figwheel provides to deliver compiled
ClojureScript files and other assets.
It is also a Clojure/Java idiom to place a resources directory on
the classpath to hold file assets that are not source files that we
will want to bundle and deploy with our application or library.
Concretely, we want to create a local resources directory and
add it to the classpath:
With Leiningen, there is no need to add resources to the classpath
as Leiningen already adds it as a default. Extra Credit: check your
Leiningen classpath and verify this for yourself.
Now that we have a resources directory on the classpath we will need
to place a public directory inside of it.
$ mkdir resources/public
Let's add a minimal robots.txt file as an example
static file that we may want to serve from the root of our webserver.
Create a resources/public/robots.txt with the following contents:
User-agent: *
Disallow: /
So now we have a project directory that looks like this:
./
├── deps.edn # or project.clj
├── resources
│ └── public
│ └── robots.txt
└── src
└── hello_world
└── core.cljs
It will be helpful to learn how to get at this file from Clojure.
The following REPL session demonstrates how you can test that files
are accessible from the classpath.
# start a REPL in the project root
$ clj
Clojure 1.9.0
user=> (require '[clojure.java.io :as io])
nil
user=> (slurp (io/resource"public/robots.txt"))
"User-agent: *\nDisallow: /\n"
user=> # control-C to exit
We were able to find the public/robots.txt file on the classpath,
this means our Webserver will be able to find it as well.
So now we have learned how to set the classpath so that the
ClojureScript compiler can find our source files and so that the
Webserver can find our static files. There is one more thing to
consider when setting up our project.
When we compile our ClojureScript source code the resulting JavaScript
output files will need a place to go and that place will need to be on
the classpath so that the webserver can serve them.
An earlier convention, and one that you can still use, was to place
the output files in the resources/public directory alongside all of
your other web assets.
While that works, figwheel.main by default places output files in
the target/public directory. Using the target directory for output
files is a convention that is already used by Leiningen. The major
reason for this is that compiled files are temporary and need to be
deleted from time to time. Leiningen has a lein clean task
that deletes the target directory and thus cleans out all compiled
and cached assets allowing us to start fresh, and perhaps eliminate
stale files that are causing problems.
Because our output files are going to be placed in the target/public
directory by default and we want them to be served as static assets by
the webserver we will need to add the target directory (NOT
target/public) to our classpath.
Adding the target directory to with Leiningen
Unfortunately I'm not going to fully explain the changes to your
project.clj file as it would require introducing quite a few ideas.
The following config is going to add a path to your classpath in
developement mode so that the files don't get added to your deployed
uberjar. It's going to use the :resource-paths because it seems to
be the most appropriate considering how we are using it. We are also
making an adjustment so that we can call lein clean without
violating a Leiningen failsafe.
If you now call clj -Spath you will see the local target directory
listed.
When working with CLI Tools we are also going to want to create the
target directory because paths on the classpath that we want to
resolve files in need to exist before the JVM starts, or file
resolution will not work.