Boot Cljs Projects
- Links
- Github Project
- Step 1 (simple cljs compilation)
- The build.boot file
- The cljs task
- Where to put cljs files?
- The resource-paths
- The serve task
- The wait task
- Running the simple build
- The index.html file
- Options, and more Options
- Optimization None
- Step 2 (target dir and init fn's)
- Where is main.js??
- The target task
- Init functions
- Init from cljs source
- Init from the html page
- Init from custom
- Step 3 (control js output)
- Controlling where the js goes
- Cljs compiler 'asset-path'
- boot-reload 'asset-path'
- Advanced Compilation
- Specifying cljs builds using ":ids"
- Summary
Links
Before reading on, if you aren't familiar with the concept of filesets in boot, I recommend you read about them first. I know there's a lot to digest there, but trust me, if you want to use boot, then reading that will really help, and it's worth it! And don't worry if you don't grasp the concept 100% the first time, it took me a while before it really clicked.
Also, here's a really awesome write up about configuring a boot clojurescript project
Github Project
You can follow along by grabbing a copy of the companion github project here:
git clone https://github.com/upgradingdave/boot-cljs-example.git
The github project has a tag for each of the Steps Below. For example, you can find the source code for Step 1
here, or, if you've cloned locally, checkout the step1
tag like so:
cd boot-cljs-example
git checkout step1
Step 1 (simple cljs compilation)
To get started, I tried to come up with the most basic project I could think of. In this step1
version of the code, there are 4 files:
build.boot
src/cljs/up/core.cljs
resources/public/css/bootstrap.min.css
resources/public/index.html
In the next few sections, I'll give an overview of what each task of the boot build will do and also describe how the index.html loads the compiled javascript.
The build.boot file
The build.boot
file sets up a single dev
task that looks like this:
(deftask dev
[]
(comp
(cljs)
(serve)
(wait)))
The dev
task runs cljs
, then serve
and then wait
.
The cljs task
In this simple version, I'm calling cljs
without any options. Which means that cljs
will compile any *.cljs
files it finds anywhere on the fileset in order to produce a main.js
file.
The way this works is that the cljs
task looks for *.cljs.edn
files anywhere in the fileset. In this case, since we haven't created a custom *.cljs.edn
file, behind the scenes, boot-cljs is creating a main.cljs.edn
file by default. In step 2 below, I show how to create you're own custom *.cljs.edn
files, but it's good to know, that by default, the cljs
task creates a main.cljs.edn
file by default for convenience. main.cljs.edn
tells cljs
to create main.js
.
By default, the cljs
task will look for and attempt to compile any cljs
files that exist anywhere on the source-paths
as well as the resource-paths
.
In this simple example, since there's only a single cljs file at src/cljs/up/core.cljs
, the cljs
task will simply compile that single cljs file in order to create main.js
at the root of the fileset.
Where to put cljs files?
The boot cljs
task will compile cljs files found in :resource-paths
and :source-paths
, so where's the best place to put them?
Here's how I distinguish between :resource-paths
and :source-paths
.
Anything found in :resource-paths
will end up as an artifact inside the final output of the build. In other words, if you put a cljs
file under :resource-paths
, the actual cljs
file will end up in the final output of the build. This is handy for creating cljs dependency jars. For example, if you want to package your cljs
files inside a jar, put them under :resource-paths
.
But, for this example, we're only interested in js files making it into the final output of the build. We don't really care about cljs files after they've been compiled into js.
So, for web projects like this example, normally, you'll probably want to put your cljs files in source-paths
. That way, only the compiled js will be available to tasks that run after cljs
task.
The resource-paths
Take a look again at build.boot
and notice I set :resource-paths
to {"resources/public"}
. This tells boot
to copy everything under the resources/public
directory and make it available to other tasks in the build pipeline. This is important because up next, we want to serve all the files under resources/public
using the serve
task.
The serve task
The boot serve
task will start a http server, and by default, it will serve the root directory of the boot fileset.
Remember that so far, boot has done the following:
- put all the files and directories found in
:resource-paths
into the fileset. - The
cljs
task has createdmain.js
inside the fileset
So now, when the jetty started from the serve
task gets an http request, the fileset (which is managed internally by boot) looks something like this:
css/bootstrap.min.css
index.html
main.js
Note that at this point, you can't actually see these files anywhere. They're being managed inside temporary directories by boot. I describe more about this later (see the section about the target task).
The wait task
The wait
task just tells boot to hang out and wait before finishing. If we didn't use wait
, the http server started by serve
would just immediately close.
Running the simple build
At this point, if we run boot dev
, we should see output like this:
boot-cljs-example> boot dev
Writing main.cljs.edn...
Compiling ClojureScript...
• main.js
2016-10-27 10:28:46.037:INFO:oejs.Server:jetty-7.6.13.v20130916
2016-10-27 10:28:46.082:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:3000
<< started Jetty on http://localhost:3000 >>
That shows that the main.js
file was compiled without any errors and that jetty is listening on port 3000 and serving all the files in the fileset.
If you're following along, you should be able to browse to http://localhost:3000.
Since index.html
is right at the root of the fileset, jetty will display the contents of index.html
and you should see the words Loading ...
in the browser.
The index.html file
Take a look at the source code of index.html file and you'll see this:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/css/bootstrap.min.css" rel="stylesheet" type="text/css">
</head>
<body>
<div id="core">Loading ... </div>
<script src="/main.js" type="text/javascript"></script>
</body>
</html>
Here's a quick walk thru of what this index.html code is doing:
- Loads the
bootstrap css
from http://localhost:300/css/bootstrap.min.css - Displays a
div
withid="core"
- Loads the
main.js
from http://localhost:3000/main.js.
Just a side note that it is indeed possible to tell boot to create main.js
somewhere other than inside the root of the fileset. More on that later.
Options, and more Options
Keep in mind, that there are two main categories of options for configuring things here. The two main types of options are:
In addition to these two different categories of options, boot-cljs
looks for options in several different places and merges them all together:
- First it looks for options inside
*.cljs.edn
files - Next it looks for options passed to the
cljs
task definition - Finally, it will automatically override some options (such as
:output-to
) because of the way boot filesets write to tmp files.
Just to reiterate, I know that :output-to
might seem like the perfect way to control where files get compiled. But, since boot maintains filesets inside temporary directories, it will most likely ignore any :output-to
values you try to set. Instead of using :output-to
, it's best to control the output locations using the *.cljs.edn
files. I'll dive into this later.
Optimization None
By default, the cljs
task will configure the clojurescript compiler to use :optimizations :none
. This means that the resulting compiled js code is human readable and that multiple js files are produced.
Remember that by default, the cljs
task will create main.js
with no optimizations.
Take a peak inside main.js
. You'll see that it loads several other js files (which were also created by the compiler). Here's what main.js
looks like:
var CLOSURE_UNCOMPILED_DEFINES = null;
if(typeof goog == "undefined") document.write('<script src="main.out/goog/base.js"></script>');
document.write('<script src="main.out/cljs_deps.js"></script>');
document.write('<script>if (typeof goog == "undefined") console.warn("ClojureScript could not load :main, did you forget to specify :asset-path?");</script>');
document.write('<script>goog.require("boot.cljs.main447");</script>');
Here's a walk thru of what main.js
attempts to load:
- First,
main.js
tries to loadgoog/base.js
from here:http://localhost:3000/main.out/goog/base.js. This is the core google closure library. This includes definitions for methods such asgoog.require
andgoog.addDependency
. - Next, it loads
cljs_deps.js
from here: http://localhost:3000/main.out/cljs_deps.js.cljs_deps.js
usesgoog.addDependency
to bring in all other dependencies
If you take a look at the source code of, cljs_deps
, you'll see something like this:
goog.addDependency("base.js", ['goog'], []);
goog.addDependency("../cljs/core.js", ['cljs.core'], ['goog.string', 'goog.object', 'goog.math.Integer', 'goog.string.StringBuffer', 'goog.array', 'goog.math.Long']);
goog.addDependency("../reagent/interop.js", ['reagent.interop'], ['cljs.core']);
goog.addDependency("../reagent/debug.js", ['reagent.debug'], ['cljs.core']);
goog.addDependency("../clojure/string.js", ['clojure.string'], ['goog.string', 'cljs.core', 'goog.string.StringBuffer']);
...
Notice that the clojurescript compiler has basically created a js file for each cljs dependency and that cljs_deps.js
is attempting to load them all using google closure machinery.
You can control the relative path of where goog/base.js
and cljs_deps.js
are loaded from by using the clojurescript compiler option :assets-path
. More on this later!
Step 2 (target dir and init fn's)
Ok, time to get a little bit more complicated, before reading on, take a look at the step2
version of the code here.
And if you're following along, run this:
git checkout step2
In this version, I've added a custom main.cljs.edn
file and also added the target
boot task. I'll use this version to talk about initializing clojurescript.
Where is main.js??
In step 1, you might notice that if you look on your hard drive where you cloned the project, you won't actually see main.js
anywhere. Yet, if you browse to http://localhost:3000/main.js, it somehow magically appears?!
Remember that boot uses the concept of filesets. And by default, these filesets are managed by boot behind the scenes inside temporary files and temporary directories. These temp files are really only meant for boot to use.
How do we get boot to actually produce a main.js
on disk that we can see? Read on about the target
task ...
The target task
In order to get boot to actually produce files (similar to the way other build tools like maven create the target directory, for example), we can use the target
task. Check it out in build.boot
from step2
:
(deftask dev
[]
(comp
(cljs)
(target)
(serve)
(wait)))
The target
task tells boot to write everything in the fileset into the boot-cljs-example/target
directory.
If you re-run boot dev
, you should see the target directory is now created.
Init functions
At this point, we have the following working:
src/cljs/up/core.cljs
file is compiled intomain.js
- the static css and html resources are being copied into the fileset
- files are being written into
target
for us to touch, taste and smell(?) - jetty is serving the files found in
target
But, our clojurescript code isn't really doing anything!
We need to actually run the main
function inside of src/cljs/up/core.cljs
.
There's a few ways to do this.
Init from cljs source
One option (and probably the most straightforward) is to simply call the (main)
function at the bottom of src/cljs/up/core.cljs
Try uncommenting the line ;;(main)
at the end of core.cljs
. After uncommenting, stop and re-run boot dev
and refresh your browser. You should see the words "Hello World!" with green background.
Sweet! Now our clojurescript is actually working.
The problem with this, however, is that now we have code that will run every time core.cljs
is loaded or required. This isn't so great and can cause confusion as the project grows.
Let's look at other options.
Init from the html page
Here's the main
method inside src/cljs/up/core.cljs
:
(defn ^:export main []
(if-let [node (.getElementById js/document "core")]
(r/render-component [hello data] node)))
That ^:export
symbol tells the clojurescript compiler to keep that main
method name intact inside the final javascript file. That way it's possible to call the clojurescript method from javascript.
So, another way to start the clojurescript is to add this to index.html
:
<script type="text/javascript">up.core.main()</script>
That's an ok way to initialize, but also not ideal. Let's try another way.
Init from custom *.cljs.edn
file
Instead of letting the cljs
task create main.cljs.edn
for us, we can have more control over the build by creating our own *.cljs.edn
file.
In step2
version of the code, I've created src/cljs/main.cljs.edn
that looks like this:
{:require [up.core]
:init-fns [up.core/main]}
This tells the cljs
task to add a call to up.core/main
at the end of the compiled main.js
file.
That way, every time main.js
is loaded by the browser, the main
function will run to start things off.
I think this is the best way to initialize the code. And you can even call multiple functions inside the :init
vector if needed.
If you start the build using boot dev
with the step2
version of the code, you should be able to browse to http://localhost:3000/. index.html
will load main.js
as before, but since we've specified :init-fn
in the main.cljs.edn
file, the up.core/main
is run in order to bootstrap our clojurescript.
Step 3 (control js output)
Take a look at the step3
version of the code here. If you're following along, run the following:
git checkout step3
In this version, I added another cljs file called src/cljs/up/simple.cljs
.
The goal of this section is to configure boot-cljs so that simple.js
is created inside the target/js
directory (instead of in the root of the target directory).
Another goal is to produce an advanced compiled version of both the core.cljs
and simple.cljs
inside the resources/public/compiled
directory.
Controlling where the js goes
The boot cljs
task will create a compiled js for each *.cljs.edn
file it finds.
To control where the js file is created is easy - just move the corresponding cljs.edn
file!
For example, in version step3
of the code, I've created a new file src/cljs/js/simple.cljs.edn
.
Notice that since I created this edn
file inside the src/cljs/js
directory, the cljs
task will use the same relative path to produce target/js/simple.js
.
But, there is a catch to this. The contents of /js/simple.js
will still contain relative paths to simple.out/goog/base.js
and simple.out/cljs_deps.js
.
Here's the issue. Unless you set :asset-path
, then when you try and load <script src=/blog/js/simple.js>
from http://localhost:3000/simple.html
, simple.js
will try to load /simple.out/goog/base.js
, and, well, that's not going to work very well.
asset-path
to the rescue!
Cljs compiler 'asset-path'
If you ever find yourself in a situation where the compiled js is created inside a subdirectory, but you forgot to set :asset-path
, then you might see the following:
Uncaught ReferenceError: goog is not defined
The solution for this is to make sure to also configure the clojurescript compiler asset-path
like this
{:require [up.simple]
:init-fns [up.simple/main]
:compiler-options {:asset-path "/js/simple.out"}}
So, just remember: if you move a cljs.edn
to a different path, don't forget to also adjust :asset-path
so that goog/base.js
and cljs_deps.js
can be found and loaded correctly.
Remember also, that this only applies when compiling with optimizations
:none
.
boot-reload 'asset-path'
Note that if you're using the boot-reload
task, it also has an option named asset-path
.
This asset-path
is meant for a subtly different purpose than the clojure compiler's asset-path
option.
Here's an example of using boot-reload
asset-path
option:
(reload :asset-path "/public/static")
This changes the behavior of boot-reload
so that instead of loading js files from http://localhost:3000/public/static/js/foo
, it will load files from http:/localhost:3000/js/foo
.
Advanced Compilation
Let's add an advanced compilation to our build!
For this, I added a new task to build.boot
called advanced
:
(deftask advanced
[]
(comp
(cljs :optimizations :advanced
:ids #{"compiled/main"})
(target)))
I also added another main.cljs.edn
file under src/cljs/compiled/main.cljs.edn
If you run boot advanced
, it should create target/compiled/main.js
.
Also, take a look inside this version of main.js
and you'll see it contains nicely optimized, compact javascript - everything it needs to run stand-alone. In other words, there's no need to worry about loading goog/base.js
or cljs_deps.js
in advanced compilation mode. There's also no need to worry about :asset-path
. Advanced compiled js files contain all the javascript necessary to run.
Specifying cljs builds using ":ids"
Notice that I configured the cljs
boot task inside advanced
with :ids #{"compiled/main"}
.
This tells the cljs task to ignore all *.cljs.edn
files except for compiled/main.cljs.edn
.
But a big heads up here! The name that you put inside the :ids
must match an existing *.cljs.edn
file include the relative path component!!!
The really confusing part for me here is that if the name put in :ids #{}
does not match any of the existing *.cljs.edn
files, the cljs will still happily create a js file in the root of the fileset. That drove me crazy for about a day and a half!
So, at this point, if you start another boot dev
build, the following should be happening:
- The
advanced
task creates advanced compiled js files undercompiled/main.js
andcompiled/simple.js
. - The
cljs
task creates development versions undermain.js
andjs/simple.js
- The
target
task writes files from the boot fileset into thetarget
directory. - The
serve
task starts jetty which serves all the files undertarget
.
I also updated index.html and added a few convenient html files to demonstrate each of the compiled javascript. You can check them out by browsing to http://localhost:3000.
Notice how the compiled versions are much more snappier to load? Also compare the js files loaded for the none
vs advanced
examples.
Summary
That's all for now. I hope that gives you deeper insight into how to use boot to manage clojurescript projects.
I hope to add sections about hot reloading using clj-reload
(like figwheel), and devcards eventually.