How to setup a Phoenix and Clojurescript project

Last week I spent some time trying to combine a project setup with the Phoenix Framework and written in Elixir with a Clojurescript project based on shadow-cljs.

Clojurescript is an amazing language to build frontend applications and the REPL-driven development experience is simply amazing.

One of the selling points about using Clojure is that you can use the same language on the client and the server. It’s true, but it’s also great to have the chance to mix different technologies and choose the ones that fit the problem you are going to solve.

Many other valid frontend technologies are used successfully with Phoenix, a popular one is Elm, which is also recommended by many members of the Elixir community (and the author of this post himself).

Let’s see how to combine this two functional programming languages in a single projects.

Everything starts with creating a new project, in this case we use mix:

mix phx.new phoenix_cljs --no-ecto

I excluded Ecto on purpose, this way you can follow this tutorial without setting up a database.

The next step is to install shadow-cljs in your assets folder:

cd phoenix_cljs/assets
yarn add --dev shadow-cljs

Then, always from the assets folder, let’s initialize a new shadow-cljs project:

node_modules/.bin/shadow-cljs init

Next step, we need to start shadow-cljs whenever we run mix phx.server. It’s very easy because Phoenix has a configuration for adding watchers and start them automatically.

Edit config/dev.exs and add a new watcher for shadow-cljs. As you note we keep using Webpack in watch mode. It’s convenient as shadow-cljs doesn’t include anything to minimize or concatenate our stylesheets.

config :phoenix_cljs, PhoenixCljsWeb.Endpoint,
  http: [port: 4000],
  debug_errors: true,
  code_reloader: true,
  check_origin: false,
  watchers: [
   node: [
      "node_modules/webpack/bin/webpack.js",
      "--mode",
      "development",
      "--watch-stdin",
      cd: Path.expand("../assets", __DIR__)
    ],
+   node: [
+      "node_modules/.bin/shadow-cljs",
+      "watch",
+      "app",
+      cd: Path.expand("../assets", __DIR__)
+    ]
  ]

Still in the same file, remove the js extension to the patterns watched by the Phoenix live reload, otherwise Phoenix will reload the full page on each change of your javascript files, overlapping on the hot reloading capabilities of shadow-cljs.

config :phoenix_cljs, 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/phoenix_cljs_web/{live,views}/.*(ex)$",
      ~r"lib/phoenix_cljs_web/templates/.*(eex)$"
    ]
  ]

Now let’s add a basic configuration for shadow-cljs, open shadow-cljs.edn and add the following lines:

;; shadow-cljs configuration
{:source-paths
 ["src"]

 :dependencies
 []

 :dev-http {9080 "../priv/static/js/"}

 :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!}}}}

Then add some dummy code to your entry point, assets/src/app/main.cljs, this way you can verify in the browser console that your setup is working:

(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))

Finally, some little changes to the Webpack configuration.

First, rename assets/js/app.js to assets/js/assets.js, as we will use this file only as an entry point to detect changes to our assets.

Then edit assets/webpack.config.js:

module.exports = (env, options) => ({
  ...
  entry: {
    "./js/assets.js": glob.sync("./vendor/**/*.js").concat(["./js/assets.js"]),
  },
  output: {
    filename: "assets.js",
    path: path.resolve(__dirname, "../priv/static/js"),
  },
  ...
});

Complete the setup including the assets.js file in your layout file app.html.eex:

<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/assets.js") %>"></script>

You are now ready to start the server:

mix phx.server

At this point the browser console should tell you that shadow-cljs is running. If you manually edit the main.cljs file hot reloading should be triggered. And if you edit a css file the page will reload too.

When releasing your application remember to run, from the /assets folder:

shadow-cljs release app

That should be all, but unfortunately there one last issue: when stopping the Phoenix server a Java process won’t be killed, which will trigger an error when you restart the server unless you kill it manually.

For now a workaround exists, create a new file called cljs-start.sh into the assets folder:

  #!/usr/bin/env bash

  # Start the program in the background
  exec "$@" &
  pid1=$!

  # Silence warnings from here on
  exec >/dev/null 2>&1

  # Read from stdin in the background and
  # kill running program when stdin closes
  exec 0<&0 $(
    while read; do :; done
    kill -KILL $pid1
  ) &
  pid2=$!

  # Clean up
  wait $pid1
  ret=$?
  kill -KILL $pid2
  exit $ret

Then in your dev.exs replace the shadow-cljs watchers configuration with:

bash: [
      "cljs-start.sh",
      "node_modules/.bin/shadow-cljs",
      "watch",
      "app",
      cd: Path.expand("../assets", __DIR__)
    ]

This new bash script will intercept the messages sent to stdin and kill Java when stdin closes.

Enjoy your Elixir + Clojurescript project.

Comments