Setting up a clojurescript project with shadow-cljs figwheel shadow-cljs and cursive
The first thing you should do is make sure you have gone through the official clojurescript tutorial. This explains how to use the clojurescript compiler to convert clojurescript to javascript, as well as get a REPL running that you can use to interact with your running program in the browser, or on NodeJS.
Other than the REPL part, this is actually pretty similar to the current JS workflow for most developers, where they use Babel to convert from ES6/JSX (i.e. a more convenient language to program in) into standard js that would be annoying to write by hand, but will run in all browsers, or on Node.
Shadow-cljs is a tool that will do the same thing you just did in the official tutorial, but with hot reloading of code into the browser. This means that when you make changes to the code, it will automatically be compiled to clojurescript, and reloaded into the browser so you can see changes in the browser almost immediately. Figwheel is another tool that does this.
I'm not really qualified to determine the differences between Shadow-cljs and Figwheel, but my heuristic (= "rule of thumb") when choosing technologies is
Use what most people use, unless you can articulate (and understand!) exactly why you should use something different
--hmm, state of clojure survey 2019 says 70% use leiningen with lein-cljsbuild, 65% use figwheel and 21% use shadow-cljs - I guess I should be using figwheel...
There are actually two versions of figwheel, lein-figwheel and figwheel-main. Figwheel-main is the newer re-write, and doesn't use leiningen. But it turns out that running a cursive clojure repl with figwheel doesn't work at this time without using leiningen, something I'm trying to avoid at this time (I only want to use one build tool, not two!) What about shadow-cljs?
[EDIT: Thomas Heller, the author of shadow-cljs kindly took at look at this post and suggested some improvements.
The first is that the first few steps below can be done more quickly by following the shadow-cljs quick start instructions. So you should definitely go read that. I'm going to leave this part of the post unchanged, just to make clear what exactly is happening at each step, in exhaustive detail! So if you just want to get up and running quickly, just follow the quick start instructions]
Step One - Make a new directory that will contain our project files
mkdir myproject
cd myproject
Step Two - Install shadow-cljs
Run the following command in the terminal, from the root of the project directory (i.e. from inside the myproject
directory:
echo {} > package.json
This will create an empty package.json file. The package.json file is used by npm to make a record of all the project dependencies we want to install in the project.
npm install shadow-cljs
This installs a copy of shadow-cljs into the project directory, into the automatically generated node_modules directory. You'll see that our package.json
file is now as shown:
{
"dependencies": {
"shadow-cljs": "^2.9.8"
}
}
and a package-lock.json
file has been automatically created - don't edit this manually, npm will change this automatically to reflect changes in the dependencies of your project.
Step Three - Set up directory structure for html and js files, and make index.html
I'm making a directory called public
containing an index.html
file, and a directory called scripts
which will contain the javascript files which are going to be loaded into the browser.
.
├── deps.edn
├── package-lock.json
├── package.json
├── public
├── index.html
└── scripts
Our index.html
file is super basic to start with:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
</body>
</html>
I plan on using Reagent, a wrapper around React so we can write our code in clojurescript instead of javascript. Then we will compile our clojurescript code into javascript code, and place it in the public/scripts/
folder, with a name of main.js
. So we need a way for the contents of main.js
to get loaded into the browser, we do that with a script tag as follows:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<script src="scripts/main.js"></script>
</body>
</html>
Note that the address of main.js
in the script tag is relative to the browser file, index.html
We also need to add a <div>
with an id, that we can use to tell react / reagent where to render the react code. So we end up with this:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="app"></div>
<script src="scripts/main.js"></script>
</body>
</html>
Step Four - Make a directory for our clojurescript files
We could just make a src
directory with a core.cljs
file in it, but then we might run into problems later.. As explained in the shadow-cljs manual,
It is therefore recommended to be very disciplined about the names you choose and properly namespacing everything. It may seem repetitive to always use (ns your-company.components.foo) over (ns components.foo) but it will save you from lot of headaches later on.
So in our case, our directory structure will be
└── src
└── com
└── myname
└── core.cljs
, not
└── src
└── core.cljs
And the start of our core.cljs
file will be
(ns com.myname.core)
corresponding to our directory structure
So now our full directory structure should look like this:
.
├── package-lock.json
├── package.json
├── public
│ ├── index.html
│ └── scripts
└── src
└── com
└── myname
└── core.cljs
Step Five - Tell shadow-cljs what to do
We do this by making a shadow-cljs.edn
file at the root of our project, which will contain a map with our configuration. You can generate a template by using npx shadow-cljs init
which gives us:
;; shadow-cljs configuration
{:source-paths
["src/dev"
"src/main"
"src/test"]
:dependencies
[]
:builds
{}}
We need to fill out values for each of the three keys, :source-paths
, :asset-path,
:dependencies
and :builds
.
Our source clojurescript files are in the src
folder, and right now, we don't want to set up separate dev
and test
source paths. so we change the value for the :source-paths
key to
{:source-paths ["src"]
...}
The :dependencies
key is fairly self-explanatory (just note that the value for :dependencies
is a vector, and each dependency is another vector within it.)
{:source-paths ["src"]
:dependencies [[reagent "1.0.0-alpha2"]]}
Finally, we want to have one build, which we name :app
. The corresponding value for the key :app
is a map
{:source-paths ["src"]
:dependencies [[reagent "1.0.0-alpha2"]]
:builds {:app { :target :browser
:output-dir "public/scripts/"
:asset-path "/scripts/"
:modules {:core {:init-fn com.myname.core/main}}}}}
The :target :browser
is not strictly required, as shadow-cljs defaults to a browser target if the :target
key and value are absent.
The :output-dir
is the directory where shadow-cljs will put the newly generated javascript files, which it creates by translating our clojurescript in the src
directory. So we set this to public/scripts/
in our case.
The :asset-path
is the relative path where the browser can find things like the js files being output by the shadow-cljs compiler, relative to the index.html
file. Since my public
folder is where my index.html
is being served from, and my :output-dir
is public/scripts/
, we need to use the relative path from /public
to public/scripts
, which is /scripts
Every build needs at least one module. The :core
key in the map sets the name of this module to :core
, which means the output js file in :output-dir
will be called core.js
, while the {:init-fn com/myname/core/main}
sets the "entrypoint" of the program to the function main
in src/com/myname/core.cljs
. The entrypoint of a the program is the first part of the program to run.
There are two keys we are going to include which are optional, but which are nevertheless extremely useful: :dev-http
and :nrepl
If we add :dev-http {8081 "public"}
to shadow-cljs.edn
, then shadow-cljs will start an http server on localhost:8081, serving files from the "public" folder when we later call npx shadow-cljs watch :app
. And if we define a specific port for the nrepl (network repl) to run on, using :nrepl {:port 55555}, it will save us a bit of work each time we start up the repl, in Cursive.
So our final shadow-cljs.edn
file looks like this:
{:source-paths ["src"]
:dev-http {8081 "public"}
:nrepl {:port 55555}
:dependencies [[reagent "1.0.0-alpha2"]]
:builds {:app { :target :browser
:output-dir "public/scripts/"
:modules {:core {:init-fn com.myname.core/main}}}}}
Step Six - Open the project in Intellij IDEA (with Cursive plugin)
Click "Create new project" from this screen:
Choose "Bare project" (because we don't want to use Leiningen or tools.deps at this point, just shadow-cljs)
In the "project location" field, hit the icon with the three dots on the right, then choose the project directory we created in the steps above. You should see the contents of the project directory.
Without selecting any of the items in the directory, click "open"
Then click "use library" on the left-hand side of the page, then click "create" to the right of the drop-down menu which currently says [No library selected]
Go to and select node_modules > shadow-cljs-jar > bin > shadow-cljs.jar, then click "open"
The drop-down menu next to "use library" should now say "Clojure SDK", and the rest of the settings should be automatically set to the correct values for our project.
[EDIT: so it turns out, per thheller that we should avoid this - instead, we should have shadow-cljs
generate a pom.xml
file and use that and Maven
to do dependency management in IDEA/Cursive.
First, go the preferences for IDEA plugins, File>Settings>Plugins on Windows/Linux, Intellij_IDEA>Preferences>plugins on macOS. Make sure that Maven
is installed
Then type the following command in the terminal:
npx shadow-cljs pom
shadow-cljs - config: /Users/myname/myproject/shadow-cljs.edn
If you do ls
in your project directory, there should be a pom.xml file there.
We now want to Choose "Open / Import" from this screen:
Navigate to the project folder, and open the pom.xml file. When asked "Open as File" or "Open as Project", click "Open as Project"]
Step Seven - run shadow-cljs
Open a terminal, and type npx shadow-cljs watch :app
. This does the following:
-
npx tells the npx package (part of npm) to run
shadow-cljs
in ournode_modules
directory. - The
watch
command:- starts the compilation process from clojurescript in our
src
directory to javascript in ourpublic/scripts/
directory, - starts a server, and
- loads the
index.html
page in thepublic
directory - starts an nrepl, a network repl that we can use to interact with the javascript runtime in the browser, by writing clojurescript in the repl, which gets translated to js and sent to the
console
in the browser in real-time.
- starts the compilation process from clojurescript in our
-
:app
is the name of the "build" that we created in Step 5, in theshadow-cljs.edn
file.
Step Eight - Set up a repl
When you typed npx shadow-cljs watch :app
, you should have seen something like the following in your terminal:
shadow-cljs - config: /Users/myname/myproject/shadow-cljs.edn
shadow-cljs - HTTP server available at http://localhost:8081
shadow-cljs - server version: 2.9.8 running at http://localhost:9630
shadow-cljs - nREPL server started on port 55555
shadow-cljs - watching build :app
[:app] Configuring build.
[:app] Compiling ...
[:app] Build completed. (153 files, 0 compiled, 0 warnings, 2.88s)
Click Run > Edit Configurations... You should see the following screen:
Click on the + sign in the top-left corner, then choose Clojure REPL > Remote
- You can change the name to anything you like (I just use "repl")
- Click nRepl for the Connection Type
- Choose "Connect to Server", and in Host, type 127.0.0.1 (this is 'localhost', but connection seemed to fail when I wrote that value in this field - so stick to the numeric IP address)
In Port, type the number that shadow-cljs printed out above (and which we set as the value of the nrepl port in
shadow-cljs.edn
above):nREPL server started on port 55555
- so in this case, we enter 55555Click OK
You should now see the word "repl" (or whatever you named your repl) in the top right of the IDEA window, with a small green triangle 'play button' next to it. Click the 'play' button to start a repl client that connects to the nrepl server started earlier by shadow-cljs
You can then type clojure code in the bottom window and when you have entered a complete clojure command (e.g (+ 1 2)
), hit the return key, you should see it evaluated in the top window. However, you'll see that this is in fact a clojure repl, not a clojurescript repl. So we need to enter two more commands in order to get a clojurescript repl that is connected to the browser. In the repl, type the following (the bit on the line after =>
is what the repl prints out after you enter the command. So you type in (shadow/watch :app)
and on the next line, the repl will print => :already-watching
)
(shadow/watch :app)
=> :already-watching
(shadow/repl :app)
To quit, type: :cljs/quit
=> [:selected :app]
After typing the second command, you should find that the dropdown menu in the repl window has changed from "clj" (clojure) to "cljs" (clojurescript).
See the Cursive repl documentation for more information, including how to send a single clojurescript expression or an entire file directly from the editor to the running repl we just set up.
Step Nine - open the browser!
You'll note that shadow-cljs also printed the following: shadow-cljs - HTTP server available at http://localhost:8081
. So we can open a browser, and navigate to http://localhost:8081
The screen will be blank, because we haven't written any reagent code, and the rest of the index.html
file is empty. But our repl is connected to this page, which we can test by entering the following into the repl:(js/alert "Hi from shadow-cljs")
which creates an alert in the browser window.
If we open the developer tools in the browser to view the js console, we can also use console.log
from the repl:
(js/alert "Hi from shadow-cljs")
=> nil
(js/console.log "hi from the repl")
=> nil
which both return nil
in the repl, but whose effects should be visible in the browser window.
Step Ten - Render some reagent / react to the browser
If we go to the Reagent webpage we can see an example of a reagent component. We can copy this component, "simple-component" to our source file core.cljs
(ns com.myname.core)
(defn simple-component []
[:div
[:p "I am a component"]
[:p.someclass
"I have " [:strong "bold"]
[:span {:style {:color "red"}} " and red "] "text."]])
But nothing shows up, why? If you have used React, you'll recall that first we need to create some html using jsx - in our case, instead we use reagent's hiccup-like syntax
But we also need to run the equivalent of ReactDOM.render()
, passing it the jsx fragment we created, as well as passing it a reference to the DOM node where we want it to show up. In Reagent, it looks pretty similar - so after importing the relevant libraries, our core.cljs looks like this:
(ns com.ghufran.core
(:require [reagent.core :as r]
[reagent.dom :as rdom]))
(defn simple-component []
[:div
[:p "I am a component"]
[:p.someclass
"I have " [:strong "bold"]
[:span {:style {:color "red"}} " and red "] "text."]])
(defn ^:export run []
(rdom/render [simple-component] (js/document.getElementById "app")))
Hmm, still not showing up in the browser. Recall that we in shadow-cljs.edn
, we had to set the entrypoint for the program. We want the run
function to run, so we change the relevant part of shadow-cljs.edn
from
:modules {:core {:init-fn com.myname.core/main}}
to
:modules {:core {:init-fn com.myname.core/run}}
For changes in shadow-cljs.edn
, you may need to refresh the page in order for the new configuration to load
At which point we should see:
Success! We can now start working our way through the reagent tutorial before moving on to re-frame...
[EDIT: After posting this, I found out that the hot reloading isn't working - oops! Thomas Heller's post is helpful - seen next post for getting hot reloading working!]