Tutorial 3 - House Keeping

August 24, 2017 · View on GitHub

In the previous tutorial we started approaching the Immediate Feedback Principle by using the boot building tool and piping a few additional tasks developed by the community:

  • boot-cljs: to compile CLJS source code (introduced in Tutorial 1);
  • boot-http: to run a CLJ based web server to serve pages;
  • boot-reload: to reload static resources when changes are saved;
  • boot-cljs-repl: to connect a CLJS REPL to the JS engine of the browser (bREPL).

In this tutorial we're going to improve the developer interaction with the boot building tool by minimizing the length of the boot commands to be submitted to the terminal, while supporting the immediate feedback style of programming.

Preamble

If you want to start working from the end of the previous tutorial, assuming you've git installed, do as follows.

git clone https://github.com/magomimmo/modern-cljs.git
cd modern-cljs
git checkout se-tutorial-02

Introduction

Let's start reviewing the boot commands we previously submitted to the terminal to progressively approach the Immediate Feedback Principle.

CLJS compilation

First, in Tutorial 1 we configured a few environment variables. Namely :source-paths and :resource-paths.

Then we launched the CLJS compilation with the following boot command:

boot cljs target

We did not pass any compilation option to the task by exploiting a few available defaults. Namely:

  • none as compiler optimization;

  • source-map defaulted to true with the none optimization

  • main.js as the name of the JS file generated by the compiler

    NOTE 1: ClojureScript supports HTML source maps so that you can debug ClojureScript directly in the browser, using the configuration option source-map. With optimizations set to none the valid values are true and false, with the default being true. With any other optimization settings you must specify a path to where the source map will be written (taken from ClojureScript Compiler Optimizations).

HTTP server

In Tutorial 2 we started by adding the serve task to the boot command. We passed it the -d target option to instruct the task about the directory to serve, the same directory name used as default by the target task. As you remember, we also needed to add the wait task for keeping the web server running.

boot wait serve -d target cljs target

CLJS recompilation

To trigger the CLJS recompilation whenever we save a modified CLJS source file, we substituted the wait task with the watch task which requires to be positioned before the cljs task.

boot serve -d target watch cljs target

boot-reload

Again, to trigger the reloading of static resources when we save their changes or changes in the CLJS source code they link, we added the reload task to the pipeline just before the cljs task.

boot serve -d target watch reload cljs target

bREPL

Last, but not least, to almost complete our intent of approaching the Immediate Feedback Principle by offering a browser base REPL (bREPL), we had to add the cljs-repl task. Don't forget to prepend the cljs-repl task to the cljs one.

boot serve -d target watch reload cljs-repl cljs target

If you complain about the things you have to remember just to start playing around your project, you're right.

This is why boot core developers offer you a very comfortable way to help your typing and your memory.

Enter deftask

From the user point of view, one of the interesting aspects of boot is the composable nature of its tasks, whether predefined by boot or not. You could spend days studying boot source code to better understand its architecture, but we want to be pragmatic. At the moment we are only interested in reducing the need to memorize task names and order while launching the boot command from the terminal.

All we have to do is to open the build.boot and define a new task as an ordered composition of other tasks by using the deftask macro as follows:

(set-env!
  ...
  ...)

(require ...
         ...)

;; define dev task as composition of subtasks
(deftask dev
  "Launch Immediate Feedback Development Environment"
  []
  (comp
   (serve :dir "target")
   (watch)
   (reload)
   (cljs-repl) ;; before cljs task
   (cljs)
   (target :dir #{"target"})))

Note the use of the comp function in the body of the newly created dev via the deftask macro. The comp function composes the same tasks in the same order we saw in the boot command. The only differences are the way the "target" values are passed to serve and to target subtasks as a :dir keyword argument instead of as an argument at the command-line. Moreover, the value passed to the target task is not just a simple string (i.e., "target"), but a set of strings (i.e., #{"target"}). At the moment we're not interested in understanding what could be the reason of this choice, even if you could suspect that boot is able to generate output in more than one directory.

The newly defined dev task is now available to be used at the command-line:

boot -h
...
         dev                        Launch Immediate Feedback Development Environment
...
Do `boot <task> -h` to see usage info and TASK_OPTS for <task>.

You can now launch the dev task as usual from the command line to get the same effect you previously obtained by composing its subtasks:

boot dev
Starting reload server on ws://localhost:60083
Writing boot_reload.cljs...
Writing boot_cljs_repl.cljs...
2015-11-04 08:58:55.742:INFO:oejs.Server:jetty-7.6.13.v20130916
2015-11-04 08:58:55.788:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:3000
<< started Jetty on http://localhost:3000 >>

Starting file watcher (CTRL-C to quit)...

nREPL server started on port 60086 on host 127.0.0.1 - nrepl://127.0.0.1:60086
Writing main.cljs.edn...
Compiling ClojureScript...
• main.js
Writing target dir(s)...
Elapsed time: 19.929 sec

You can then repeat all the tests already shown in Tutorial 2 including the bREPL creation from a nrepl client.

Not bad. You now have a very simple command, boot dev, to launch a development environment approaching the Immediate Feedback Principle.

When you finish with your experimenting, kill everything.

Defaults overwriting

As told from the very beginning, we tried to adhere as much as possible to some boot tasks' defaults with the intent of simplifying the structure layout of the project. That said, even if some defaults are sane enough to be adopted for a very simple project like the one we started from, as soon as the project gets more articulated you'll need to overwrite a few of them.

Most webapps organize their HTML/CSS/JS resources to keep them separated from each other. Just to make an example, most of the webapps group their JS resources under a js subdirectory living in the directory served by the web server and link those js resources in the corresponding <script> html tag.

Let's try to adhere to this convention by first editing the html/index.html file and modifying the link to the main.js file generated by the CLJS compilation phase.

<!doctype html>
<html>
  <head>
    <title>Hello, World!</title>
  </head>
  <body>
    <script src="js/main.js"></script>
  </body>
</html>

If you take a look at the clojurescript compiler options you'll discover that you need two options to be passed to the CLJS compiler to support this new files organization:

  • :output-to: sets the path to the JavaScript file that will be output
  • :asset-path: sets a path relative to the directory served by the web server.

How could we pass these options to the boot-cljs which launches the CLJS compiler? You could easily go crazy trying to solve this very simple problem by reading the boot-cljs documentation. Some compiler options can be set at the task level, others can be set by defining appropriate edn files located in the right position in the resources directories (e.g. into the html directory in our project).

Let's not go into too many details about the reasons that boot-cljs supports the compiler options the way it does; instead, let's be pragmatic again and just list what you need to do:

First you have to create the js subdirectory in the html directory containing the html pages.

cd pathname/to/modern-cljs
mkdir html/js

Then create a cljs.edn file in the newly created js directory reflecting the name of the JS file generated by the CLJS compiler.

touch html/js/main.cljs.edn

Now edit that file as follows:

{:require [modern-cljs.core]
 :compiler-options {:asset-path "js/main.out"}}

As you see we just required the modern-cljs.core namespace of the project and set the asset-path option to the CLJS compiler. If the output-to option is not specified default value is created based on relative path of the .cljs.edn file. This way we have completed configuration for CLJS compiler. That's it.

You can now run the CLJS compilation. To see the very verbose output of the CLJS compilation task, use the -vv option of the boot command as follows:

boot -vv cljs target
...
Compiling ClojureScript...
...
 js/main.js
CLJS options:
{:asset-path "js/main.out",
 :output-dir
 "/Users/mimmo/.boot/cache/tmp/Users/mimmo/tmp/modern-cljs/3c5/-w7zxdu/js/main.out",
 :output-to
 "/Users/mimmo/.boot/cache/tmp/Users/mimmo/tmp/modern-cljs/3c5/-w7zxdu/js/main.js",
 :main boot.cljs.main583}
...

This way you can verify that the CLJS compiler correctly generated the main.js file in the js subdirectory relative to the directory served by the web server and that it sets the js/main.out subdirectory as the value of the asset-path compiler option.

Now take a look at the layout of the target directory to confirm that the compiler options worked as expected

target
├── index.html
└── js
    ├── main.cljs.edn
    ├── main.js
    └── main.out
        ├── boot
        ...
        └── modern_cljs
            ├── core.cljs
            ├── core.cljs.cache.edn
            ├── core.js
            └── core.js.map

Finally, repeat all the manual tests we introduced in the Tutorial 2 to verify that the Immediate Feedback Development Environment is still working.

Launch the boot dev command:

boot dev
Starting reload server on ws://localhost:52434
Writing boot_reload.cljs...
Writing boot_cljs_repl.cljs...
2015-11-04 17:26:11.291:INFO:oejs.Server:jetty-7.6.13.v20130916
2015-11-04 17:26:11.445:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:3000
<< started Jetty on http://localhost:3000 >>

Starting file watcher (CTRL-C to quit)...

Adding :require adzerk.boot-reload to main.cljs.edn...
nREPL server started on port 52437 on host 127.0.0.1 - nrepl://127.0.0.1:52437
Adding :require adzerk.boot-cljs-repl to main.cljs.edn...
Compiling ClojureScript...
• js/main.js
Writing target dir(s)...
Elapsed time: 25.922 sec

Now launch the nrepl client in a new terminal:

boot repl -c
REPL-y 0.3.7, nREPL 0.2.12
Clojure 1.7.0
Java HotSpot(TM) 64-Bit Server VM 1.8.0_66-b17
        Exit: Control+D or (exit) or (quit)
    Commands: (user/help)
        Docs: (doc function-name-here)
              (find-doc "part-of-name-here")
Find by Name: (find-name "part-of-name-here")
      Source: (source function-name-here)
     Javadoc: (javadoc java-object-or-class-here)
    Examples from clojuredocs.org: [clojuredocs or cdoc]
              (user/clojuredocs name-here)
              (user/clojuredocs "ns-here" "name-here")
boot.user=>

Then launch the bREPL:

boot.user=> (start-repl)
<< started Weasel server on ws://127.0.0.1:52439 >>
<< waiting for client to connect ... Connection is ws://localhost:52439
Writing boot_cljs_repl.cljs...

An finally visit the http://localhost:3000 URL to activate the bREPL

 connected! >>
To quit, type: :cljs/quit
nil
cljs.user=>

Play around as you like. When finished, quit everything and reset your git branch.

git reset --hard

Next step - Tutorial 4: Modern ClojureScript

In the next tutorial 4 we're going to have some fun introducing form validation in CLJS.

License

Copyright © Mimmo Cosenza, 2012-2017. Released under the Eclipse Public License, the same as Clojure.