Recently I was tasked with setting up a project with ClojureScript and Phoenix. I tried to check the internet for a "how-to" but did not find an up-to-date guide on completing this setup. After some research and trials, I was finally able to get it done so I decided to share the knowledge I have gained.
ClojureScript is a functional language that solves a lot of the shortcomings and inconsistencies of JavaScript. It compiles to JavaScript and it is possible to call any JavaScript function with ClojureScript syntax.
Phoenix is an awesome web framework that is built on Elixir, a general-purpose, functional language built on the Erlang's VM and compiles to Erlang bytecode.
To set up ClojureScript and Phoenix, we can use an existing Phoenix project or set up a new Phoenix project by running
mix phx.new cljs_phoenix --no-ecto
We use the --no-ecto
flag because we do not want to set up a database in this project.
We will be making use of the shadow-cljs
package to compile our ClojureScript code, so we will install shadow-cljs
as a dev dependency in the assets
folder of our project.
cd assets && yarn init && yarn add --dev shadow-cljs
Up next, we will initialize a new shadow-cljs
project in the assets
folder by running
node_modules/.bin/shadow-cljs init
Running the above command displays a prompt Create? [y/n]:
. Type y
and click enter to proceed.
This generates a shadow-cljs.edn
file. This file is what we will use to configure the build of our ClojureScript code and to manage our dependencies. You can liken it to what package.json
does for NodeJs projects.
Before we add our configurations to the shadow-cljs.edn
file, let us first create a ClojureScript source file. We will create a src
folder in the assets
folder. This will be the source directory for ClojureScript. Next, we create a namespace, app.main.cljs
. Based on the Java Naming Conventions, this namespace will be placed in the app/main.cljs
file. So we create the app/main.cljs
file in our src
folder and paste the following code into it.
(ns app.main)
(def value-a 1)
(defonce value-b 2)
(defn main! []
(println "App loaded!"))
(defn reload! []
(println "Code updated.")
(println "Trying values:" value-a value-b))
This is a simple script that logs some text to the console. At the end of this guide, we will see the texts displayed in our console.
Now let’s revisit the shadow-cljs.edn
file to configure our build. We’ll paste the following code into the file.
;; shadow-cljs configuration
{:source-paths
["src"]
:dependencies
[]
:dev-http {9080 "../priv/static"}
:builds {:app {:output-dir "../priv/static/js"
:asset-path "/js",
:target :browser
:modules {:app {:init-fn app.main/main!}}
:devtools {:after-load app.main/reload!}}}}
As we can see, this is a map of key-value pairs. This configuration is customizable, so we will go through what each of the configurations does so that you can either use it directly or edit it to suit your needs.
- The
:source-paths
configures the classpath. The compiler will make use of this configuration to find Clojure(Script) source files (eg..cljs
). It is possible for us to use multiple source paths. For example, if we want to separate our tests from the main code, we could include the source path for our test and our main code as below.
{:source-paths ["src/main" "src/test"]
...}
The
:dependencies
field will contain the list of dependencies we require in the project. For now, we are not using any dependency so the field is an empty array.The
:dev-http
field expects a map of port-number to config.shadow-cljs
provides additional basic HTTP servers that serve static files. The reason for this is that some browsers may not be able to access files in some folders, but when these files are served over HTTP, the browser can easily access them by<domain>:<port-number>/<path-to-file>
. In our configuration, we made our port number9080
(we can use any available port number). We mapped the port number to"../priv/static"
. This tellsshadow-cljs
to serve all the files in"../priv/static"
over HTTP, and make them accessible to the browser on port9080
. We can use any path for this. We chose this path because our Phoenix application already has a configuration that makes all the files inpriv/static
accessible at the"/"
path. We can see that in theCljsPhoenixWeb.Endpoint
module.
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phx.digest
# when deploying your static files in production.
plug Plug.Static,
at: "/",
from: :cljs_phoenix,
gzip: false,
only: ~w(assets fonts images favicon.ico robots.txt)
We specify the build configurations in the
:builds
field. We can specify multiple builds with different ids, but in our current configuration, we only have one build -:app
. Let us take a look at the configurations we specified for the:app
build.:output-dir
specifies the directory used for all compiler output. This is where the entry point JavaScript file and all related JS files will appear:asset-path
is the relative path from the web server’s root to the resources. The dynamic loading during development and production needs this path to correctly locate files. Since we are serving our web server in the folder"../priv/static"
, and our:output-dir
is"../priv/static/js"
, it means that our:asset-path
will be"/js"
:target
specifies the target environment for the compilation:modules
configure how the compiled source code are bundled together and how the final.js
are generated.
For more information about the build configurations and dependency managements, check the docs.
We will need to update the CljsPhoenixWeb.Endpoint
module to add js
to the list of folders in priv/static
that will be served from "/". This module is in the lib\cljs_phoenix_web\endpoint.ex
file
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phx.digest
# when deploying your static files in production.
plug Plug.Static,
at: "/",
from: :cljs_phoenix,
gzip: false,
- only: ~w(assets fonts images favicon.ico robots.txt)
+ only: ~w(assets fonts images favicon.ico robots.txt js)
Since shadow-cljs
is capable of hot-reloading, we need to remove the .js
extension from the patterns watched by the Phoenix live reload. We do this in the config/dev.exs
file
# Watch static and templates for browser reloading.
config :cljs_phoenix, PhoenixCljsWeb.Endpoint,
live_reload: [
patterns: [
- ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
+ ~r"priv/static/.*(css|png|jpeg|jpg|gif|svg)$",
~r"priv/gettext/.*(po)$",
~r"lib/cljs_phoenix_web/(live|views)/.*(ex)$",
~r"lib/cljs_phoenix_web/templates/.*(eex)$"
]
]
After compilation, the build we specified for :app
will create an app.js
file in the output directory, "priv/static/js"
Since we want to load this in the browser we will include this javascript in our root HTML file, lib\cljs_phoenix_web\templates\layout\root.html.heex
.
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/js/app.js")}></script>
Finally, we need to start the shadow-cljs
server every time we start the Phoenix server. We do this by adding the following watcher to the list of watchers in the config/dev.exs
file.
config :cljs_phoenix, CljsPhoenixWeb.Endpoint,
watchers: [
# ...,
bash: [
"node_modules/.bin/shadow-cljs",
"watch",
"app",
cd: Path.expand("../assets", __DIR__)
]
]
What this does is run node_modules/.bin/shadow-cljs watch app
in the assets
folder. This will start the shadow-cljs
server, and compile the ClojureScript files according to the configurations in the shadow-cljs.edn
file. Then it watches for changes in ClojureScript files to update the compiled code.
Now, we are ready to start our Phoenix server. We ensure we are in the root folder of our project (not the assets
) folder, then we run
mix phx.server
To be sure that we have successfully set up the project, we go to localhost:4000
in our browser. In the console, we should see a prompt that shadow-cljs
is ready. We will also see the "App loaded!"
text that we printed in the src/app/main.cljs
file.
Whenever we want to release our application, in the assets
folder, we will run
shadow-cljs release app