Add ClojureScript to MilesToGo
MilesToGo is a personal location tracking app I am making to track cars, bikes etc that you own and then checkout all the places you visited. Currently it only has authentication. But I intend to have a nice little implementation in a month. I will mostly be talking about that in here!
In this post I am going to describe the steps I took to include ClojureScript to MilesToGo
. The steps are:
- Remove Webpack, node_module, and all JS code
- Add Parcel (mostly for SCSS packaging)
- Add ClojureScript along with its
app.js
equivalent with all live-view initialization
So let’s start
Removing JS Code
- First, I removed everything under the
assets/js/
folder, along with package-lock.json
and webpack.config.js
- In
package.json
I removed everything in devDependencies
At this point the system is broken in the front-end section. iex -S mix phx.server
will spawn the Elixir process fine, but will complain that it couldn’t start the Node
watcher. You will still be able to start and see the app as it is, because priv/static/*
, so I went ahead and deleted those too. Now it’s all a mess! Let’s clean it.
Add Parcel
This bit is optional, I could just keep webpack
and let it do the work, but Parcel
is simpler and since all I need it for is to pack the styles, why not keep it minimal and less complected?
-
cd
into assets/
-
npm install --save-dev parcel
(And hello vulnerability warning!)
- Made package.json’s
script
-s look like this:
{
...
"scripts": {
...
"deploy": "cp -R static/* ../priv/static/ && parcel build js/assets.js --dist-dir ../priv/static/css --public-url /dist --no-cache",
"watch": "cp -R static/* ../priv/static/ && parcel watch js/assets.js --dist-dir ../priv/static/css --public-url /dist"
},
...
}
- What should’ve been
app.js
is now asseta.js
. Because that is what it acts like. And asset manager. So I went ahead and created an assets.js
with just one line of code: import css from "../css/app.scss";
By now I had Parcel
set up. But Phoenix needs to know when/how to reload these things.
- Now, add
npm install --save-dev parcel-plugin-stdin
because well, to kill node processes and mimic --watch-stdin
functionality upon server stops. (Or something along the line, I didn’t care to read more about it at this time)
- Added
npm: ["run", "watch", cd: Path.expand("../assets", __DIR__)]
in dev.exs
in the first MilesToGo.Endpoint
config. Replacing the webpack
friendly one.
- In
root.html.leex
, I renamed <link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
to <link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/assets.css") %>"/>
- If I run Phoenix now, I will see all the colors coming back. None of the live-views though. We need to get ClojureScript for that.
Add ClojureScript
-
CD
into assets/
- I never thought I’d be loving an
npm
command this much. I went ahead and typed npm install --save-dev shadow-cljs
to kick things off. Also, no vulnerabilities.
- Next, I initialized shadow-cljs with
node_modules/.bin/shadow-cljs init
. A wild shadow-cljs.edn
appeared.
- This totally needs to be a watcher in
dev.exs
. So, added the following watcher to dev.exs
right below the Parcel one (This will change a few steps later though):
node: [
"node_modules/.bin/shadow-cljs",
"watch",
"app",
cd: Path.expand("../assets", __DIR__)
]
- I realized if I watch all the
js
patterns inside priv/static/
I will be overwhelmed at all the files that will be generated, however, I do need to watch just for one file, the resultant app.js
, so I went ahead and removed the js
from the patterns and added app.js
like so:
...
live_reload: [
patterns: [
~r"priv/static/js/app.js$", # Add the app.js
~r"priv/static/.*(css|png|jpeg|jpg|gif|svg)$", # Remove the js pattern
...
]
]
- Now in Clojure land, we need to mention where and how to generate files. And that’s need to be in our
shadow-cljs.edn
file. So, I went ahead and added things in builds
. The file now looks like:
;; 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!}}}}
- Now let’s test things, and for that, let’s add a ClojureScript file inside
assets/src/app/main.cljs
. Just to see folks are all friendly with each other:
;; assets/src/app/main.cljs contents. For now. This will house live-view things in a bit.
(ns app.main)
(defn main! []
(println "App loaded!"))
- Now the problem with me was, I kept getting error claiming “Already Started”, because killing Phoenix didn’t seem to have killed the
java
process. So as per the tutorial, I added this file and changed my watcher
to reflect it. (The file is /assets/cljs-start.sh
)
#!/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
And my watcher
looked like (Remember that file I said I’ll change?):
watchers: [
...
bash: [
"cljs-start.sh",
"node_modules/.bin/shadow-cljs",
"watch",
"app",
cd: Path.expand("../assets", __DIR__)
]
]
- Now if I run
iex -S mix phx.server
, open the site and console, I will see a nice "App Loaded"
message.
Yay! I successfully managed to add ClojureScript in a Phoenix project, thanks to shadow-cljs
Enter LiveView
-
main.cljs
is our person to write whatever ClojureScript we want in. Remember the app.js
with all the liveview.connect()
etc? We need those in ClojureScript. Without further ado, I’ll just add what’s in the file:
(ns app.main
(:require
["nprogress" :as nprogress]
["phoenix_html" :as phoenix_html]
["phoenix" :refer [Socket]]
["phoenix_live_view" :refer [LiveSocket]]))
(def csrf-token (-> "meta[name='csrf-token']"
(js/document.querySelector)
(.getAttribute "content")))
(def live-socket-params
(clj->js {:params {:_csrf_token csrf-token}}))
(set!
(.. js/window -liveSocket)
(LiveSocket. "/live" Socket live-socket-params))
(def live-socket (.. js/window -liveSocket))
(defn main! []
(.connect live-socket))
Now we have a working example of LiveView + ClojureScript.
There are some thing missing out though:
- NProgress?
- There were some JS kung-fu to manage closing of alerts and browser burgers etc.
I will be back with those soon!
In the meantime, the code is in Github! Here’s the PR. It is incomplete but if you want a more full ClojureScript + LiveView implementation, there’s this.