How to Set up A ClojureScript and Phoenix Project

How to Set up A ClojureScript and Phoenix Project

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 number 9080 (we can use any available port number). We mapped the port number to "../priv/static". This tells shadow-cljs to serve all the files in "../priv/static" over HTTP, and make them accessible to the browser on port 9080. We can use any path for this. We chose this path because our Phoenix application already has a configuration that makes all the files in priv/static accessible at the "/" path. We can see that in the CljsPhoenixWeb.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