So how does your tiny ClojureScript program transform into runnable JavaScript?
(ns app.core)
(println "Howdy!")
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
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)"
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"}}}
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
If this runs, it means you are hooked into Node process, nice!
(js/console.log "hello")
Try to list files in the current directory using Node'js fs module.
(def fs (js/require "fs"))
(.readdirSync fs "./")
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!")
Now let's compile the program into Node script
clj -M -m cljs.main --target node --output-dir ./out --compile app.core
and run the script
node out/main.js
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
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
// 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
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
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")))
and update your src/app/core.cljs to
(ns app.core
(:require-macros [app.core :refer [version]]))
(println "Howdy!" (version))
Now init a git repo in your project directory and make the first commit
git init
git add src
git commit -m "first commit"
Compile your ClojureScript project
clj -M -m cljs.main --target node --output-dir ./out --optimizations advanced --compile app.core
and run it
node out/main.js
The script should print last git commit hash. Now inspect JavaScript generated from src/app/core.cljs
cat out/app/core.js
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)
);
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)