1. Links
  2. Github Project
  3. Step 1 (simple cljs compilation)
    1. The build.boot file
    2. The cljs task
    3. Where to put cljs files?
    4. The resource-paths
    5. The serve task
    6. The wait task
    7. Running the simple build
    8. The index.html file
    9. Options, and more Options
    10. Optimization None
  4. Step 2 (target dir and init fn's)
    1. Where is main.js??
    2. The target task
    3. Init functions
    4. Init from cljs source
    5. Init from the html page
    6. Init from custom
  5. Step 3 (control js output)
    1. Controlling where the js goes
    2. Cljs compiler 'asset-path'
    3. Advanced Compilation
    4. Specifying cljs builds using ":ids"
  6. 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:

  1. put all the files and directories found in :resource-paths into the fileset.
  2. The cljs task has created main.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:


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:

  1. boot-cljs options
  2. clojurescript compiler options.


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:

  1. First, main.js tries to load goog/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 as goog.require and goog.addDependency.
  2. Next, it loads cljs_deps.js from here: http://localhost:3000/main.out/cljs_deps.js. cljs_deps.js uses goog.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 into main.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.

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 under compiled/main.js and compiled/simple.js.
  • The cljs task creates development versions under main.js and js/simple.js
  • The target task writes files from the boot fileset into the target directory.
  • The serve task starts jetty which serves all the files under target.

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.