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:
-
noneas compiler optimization; -
source-mapdefaulted totruewith thenoneoptimization -
main.jsas the name of the JS file generated by the compilerNOTE 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 tononethe valid values aretrueandfalse, with the default beingtrue. 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.