DEV Community

Cover image for Advanced Beginner’s guide to ClojureScript
Roman Liutikov
Roman Liutikov

Posted on • Originally published at romanliutikov.com

Advanced Beginner’s guide to ClojureScript

So how does your tiny ClojureScript program transform into runnable JavaScript?

(ns app.core)

(println "Howdy!")
Enter fullscreen mode Exit fullscreen mode

First of all, you need a compiler! ClojureScript is a hosted language, which means you have to compile it into JavaScript, unless you want to ship the compiler into a browser to be able to interpret ClojureScript at runtime, which is slow and in most cases doesn't make sense (but maybe you wanna build online REPL, that's ok).

This is not going to be easy. ClojureScript sits on top of JavaScript, the compiler is a library written in Clojure, which in turn hosts on JVM. This means you need Java installed, huh.

I prefer to use sdkman to manage my Java installations.

sdk install java
Enter fullscreen mode Exit fullscreen mode

Next step is to install Clojure, head over to installation guide at clojure.org. If the following command returns 2, you are good!

clj -M -e "(inc 1)"
Enter fullscreen mode Exit fullscreen mode

Create project directory somewhere and put deps.edn file into it with the following contents. deps.edn is kind of package.json, if you are coming from JavaScript world.

{:deps {org.clojure/clojurescript {:mvn/version "1.12.42"}}}
Enter fullscreen mode Exit fullscreen mode

I hope you have Node installed? Now we can run ClojureScript REPL in Node environment. This line runs JVM process that loads Clojure, that loads ClojureScript compiler, starts Node process and runs it in REPL mode.

clj -M -m cljs.main --target node --repl
Enter fullscreen mode Exit fullscreen mode

If this runs, it means you are hooked into Node process, nice!

(js/console.log "hello")
Enter fullscreen mode Exit fullscreen mode

Try to list files in the current directory using Node'js fs module.

(def fs (js/require "fs"))
(.readdirSync fs "./")
Enter fullscreen mode Exit fullscreen mode

It works? Great, let's move onto actually compiling a project into JavaScript files. Put our tiny program into src/app/core.cljs file. src is your project's source root directory. Also note that namespace name app.core (that's how modules are called in Clojure) in the code looks exactly like app/core.cljs. That's a convention in Clojure projects: namespace names should reflect directory structure starting from project's source root directory.

(ns app.core)

(println "Howdy!")
Enter fullscreen mode Exit fullscreen mode

Now let's compile the program into Node script

clj -M -m cljs.main --target node --output-dir ./out --compile app.core
Enter fullscreen mode Exit fullscreen mode

and run the script

node out/main.js
Enter fullscreen mode Exit fullscreen mode

Do you see the output? Great, let's stop here and inspect compiler's output. Specified out directory includes whole bunch of files.

ls out

app cljs cljs_deps.js cljsc_opts.edn goog main.js nodejs.js nodejs.js.map nodejscli.js nodejscli.js.map
Enter fullscreen mode Exit fullscreen mode

The actual compiled JavaScript program from your src/app/core.cljs file is in out/app/core.js, let's see what's inside

cat out/app/core.js
Enter fullscreen mode Exit fullscreen mode
// Compiled by ClojureScript 1.12.42 {:target :nodejs, :nodejs-rt true, :optimizations :none}
goog.provide("app.core");
goog.require("cljs.core");
cljs.core.println.call(null, "Howdy!");

//# sourceMappingURL=core.js.map
Enter fullscreen mode Exit fullscreen mode

cljs.core.println.call(null, "Howdy!"); is your (println "Howdy!"), where println is actually a part of implicit cljs.core namespace, which provides stdlib.

goog.provide and goog.require is a part of JavaScript module format of Google's Closure Compiler. Wait, what? Another compiler? Yeah, you see... ClojureScript compiler takes your source and outputs equivalent JavaScript code. Similarly to out/app/core.js, there's also out/cljs/core.js file which is compiled stdlib of ClojureScript. This just means that ClojureScript compiler is a source to source compiler. But how do we bundle all those JavaScript files into production ready bundle?

That's the job of Closure Compiler. Closure is an optimizing JavaScript compiler that ClojureScript is using since its initial release, in 2011. At the time JavaScript didn't have standard module format, remember AMD, UMD, RequireJS and CommonJS? Closure folks at Google invented another one, where goog.provide declares a module and goog.require imports another module.

ClojureScript emits Closure's module format so that the compiler can pick up and optimize generated JavaScript. Putting everything together, that's the stack you have to deal with:

To plug Closure into the equasion and produce optimized JavaScript bundle run the following command.

--optimizations advanced option is what tells Closure to gather all generated JavaScript files into a single bundle and optimize the program by removing unused code. Closure is quite powerful compiler, you can learn more about all kinds of optimizations it performs in this handbook that I created a while ago.

clj -M -m cljs.main --target node --output-dir ./out --optimizations advanced --compile app.core
Enter fullscreen mode Exit fullscreen mode

Output out/main.js file is now a self-contained JavaScript program.

Let's make something cool now. Put the following code into src/app/core.clj

(ns app.core
  (:require [clojure.java.shell :as sh]))

(defmacro version []
  (:out (sh/sh "git" "rev-parse" "--short" "HEAD")))
Enter fullscreen mode Exit fullscreen mode

and update your src/app/core.cljs to

(ns app.core
  (:require-macros [app.core :refer [version]]))

(println "Howdy!" (version))
Enter fullscreen mode Exit fullscreen mode

Now init a git repo in your project directory and make the first commit

git init
git add src
git commit -m "first commit"
Enter fullscreen mode Exit fullscreen mode

Compile your ClojureScript project

clj -M -m cljs.main --target node --output-dir ./out --optimizations advanced --compile app.core
Enter fullscreen mode Exit fullscreen mode

and run it

node out/main.js
Enter fullscreen mode Exit fullscreen mode

The script should print last git commit hash. Now inspect JavaScript generated from src/app/core.cljs

cat out/app/core.js
Enter fullscreen mode Exit fullscreen mode

The hash is embedded in the code!

// Compiled by ClojureScript 1.12.42 {:static-fns true, :optimize-constants true, :target :nodejs, :nodejs-rt true, :optimizations :advanced}
goog.provide("app.core");
goog.require("cljs.core");
goog.require("cljs.core.constants");
cljs.core.println.cljs$core$IFn$_invoke$arity$variadic(
  cljs.core.prim_seq.cljs$core$IFn$_invoke$arity$2(["Howdy!", "7f7e8c3\n"], 0)
);
Enter fullscreen mode Exit fullscreen mode

That's the cool part about Lisps. You can write code in your program that runs at compile time on a program itself. It's almost like a compiler inside of a compiler, except that you don't need special plugins to traverse AST, in Clojure and Lisps in general you create macros, special functions that take code, transform it and return new code.

Now, everything that you learned here is only a part of the story. We haven't touched on the language itself, how real projects are built and how ClojureScript integrates with JavaScript's ecosystem of NPM packages. ClojureScript is definitely not the easiest beast to pick up, but if you are doing front-end development and feel like digging into alternatives and learning from other languages, then there are definitely a few things you can learn from!

Top comments (0)