Our thoughts, knowledge, insights and opinions

How we embedded ClojureScript in our GitHub pages

Ever wondered if you can embed ClojureScript in a GitHub blog? The answer is: of course you can! At the end of the day, ClojureScript just translates to plain old JavaScript that can be included in any web page.

Why would you want that?

Here at Scalac, we are always trying to be innovative and creative in everything we do. This is why when I heard that we were planning to write some blog post on Clojure/ClojureScript, I asked myself: why can’t I write a ClojureScript post in ClojureScript itself?

The advantage of this is evident, our pages will be way more interactive and therefore interesting to read, play with and learn from.

How does the ClojureScript compiler work?

In order to start, I want first to give the reader some general notion on how the compiler works and why it is so powerful. I am not going to spend too much time on it because you can find better detailed articles out there.

The most significant difference between Clojure and ClojureScript is that ClojureScript concretely isolates the code interpretation phase (reading in Lisp terms) from the actual compilation phase (analysis and emission). The text that a programmer writes is read by the ClojureScript reader, then passed to the macro expansion stage and ultimately to the analyzer. The outcome is the abstract syntax tree (AST), a tree-like metadata-reach version of it.

This is paramount for a bunch of obvious reasons, but mostly because you have separation of concerns between tools that understand text and tools that understand ASTs, like compilers. Indeed this is why ClojureScript can actually target disparate kinds of platforms: in order to emit instructions belonging to the X programming language you “only” need your own compiler to read the AST, prune it and emit. This is also how currently the Google Closure Compiler is employed in order to emit optimized JavaScript.

Automating post creation

GitHub blog posts are nothing but standard .md files that Jekyll builds locally and then serves. The Markdown syntax allows adding standard HTML tags and consequently JavaScript <script> tags. Therefore, embedding ClojureScript was relatively easy once I got pass configuring it. This is mainly why I thought writing a blog post might be a good idea and save some folk’s time.

I wanted to be able to plug my changes in transparently, still allowing to create posts without Clojure. A simple start.sh was previously used to create .md files in Jekyll’s _posts folder that then had to be worked on by the author and committed.

Instead, I needed to create fully-fledged ClojureScript projects, potentially one per blog post, somewhere. I chose to hide them in a brand new _cljs folder as git submodules and for this reason I added a few lines to start.sh for:

  1. Performing git submodule add.

  2. Materializing the project with lein new <template> <project-name>.

  3. As before, copy the .md template to _posts.

Now switching to the project folder was showing me the reassuring sight of the project.clj file.

Configuring and building

I wanted to be smarter about emitting JavaScript and tried to deploy directly inside Jekyll’s scripts folder. According to my template default, the JavaScript was emitted to <project-name>/resources/public/js/compiled so I changed my <project-name>/project.clj to:

Remember that we are inside a project folder under _cljs, therefore Jekyll’s root is two directories up. Typically, only :output-to is significant for the final version as this option contains the path of the generated (and minified) .js file. In jekyll-dev though, you can also specify :output-dir, which is where temporary files used during compilation are written, and :asset-path, that sets where to find :output-dir files at run-time. This way you have full visibility of the output.

Now I was finally able to cd to my project, execute lein cljsbuild once jekyll, and see my generated .js in scripts. Hooray!

The magic bit

The last piece of the puzzle was to run the actual JavaScript code inside the Markdown blog page. There are many ways to do this, but the one I found most intuitive and straightforward was by using reagent. This is not a post about reagent per se (we wrote about it some time ago), but its lean and unopinionated architecture struck me as the way to go. Reagent, a React wrapper, dynamically mounts DOM elements and re-renders them when necessary, effectively hiding the complications of managing React component’s life cycle.

Consequently, on the HTML side I needed to: define a mounting point, include the compiled .js and trigger the JavaScript main() which mounts my app. My .md became:

Note that it is very important to prepend a slash to script and replace dashes with underscores in the last JavaScript call. The reason is that the compiler always transforms namespace dashes in underscores.

On the ClojureScript side instead I needed to ensure that the <div> with id cljs-on-gh-pages was correctly mounted:

Now every time the blog post page is shown, reagent intercepts the div and renders anything our main() returns, typically Hiccup-crafted react components, like page above.

The perk

If you have had the patience of reading till the end, here is a reward for you: a ClojureScript REPL to toy with!

Thanks to highly skilled ClojureScript hackers and Clojure being a homoiconic language, it is not surprising that it can compile itself and run into a self-hosted environment.

Self-hosted means that the language provides itself the environment where to run, which in this case is the set of JavaScript functions that are performing the code evaluation. The JavaScript, in turn, being compiled from ClojureScript source. Convoluted and awesome.

Not everything works in this ClojureScript-in-ClojureScript habitat at the moment. However, thanks to other inspiring implementations, here too you have access to Clojure’s superpowers. Note that the REPL has history (up to start, up/down to navigate) plus other handy shortcuts. Enjoy!



You like this post? Want to stay updated? Follow us on Twitter or subscribe to our Feed.