ClojureScript is an incredible new project that any Clojurian can appreciate. What could be better than replacing vexing JavaScript with the clean, flowing syntax of Clojure? But ClojureScript is very new, and in some cases its lack of polish can be frustrating. One of these frustrating aspects is ClojureScript's failure to complete abstract JavaScript using Clojure syntax. This comes from the fact that ClojureScript does not currently have all the functionality of its counterpart. For example, Clojure's bound?
function is not implemented in ClojureScript (see "bound?" in ClojureScript devnotes). That may seem trivial but if you're hoping to transfer a piece of Clojure code to ClojureScript, and that code has a bound?
call, you must learn additional aspects of ClojureScript to get your code to work. You will also have to maintain the new ClojureScript version of your code along with your pre-existing Clojure version. These small differences can add up and before you know it you could be maintaining two separate Libraries. The kicker is of course that these libraries have about 95% code in common but the 5% of difference simply cannot be resolved.
We encountered this problem here at 8th Light while extending our Clojure testing framework Speclj to ClojureScript. There were numerous ClojureScript incompatibilities sprinkled throughout the existing Clojure library. In our original solution, we built a pre-compiler that would run against the Clojure code. This program could switch out all of the incompatibilities with their ClojureScript equivalents. However, the solution came with a few major drawbacks. First, the pre-compiler was a one-off design, built specifically for the Speclj project. There was little chance that it would be maintained, which introduced an added layer of fragility to our project. More importantly, the ClojureScript version still was its own library. This led to the confusing distinction between Speclj (Clojure) and Specljs (ClojureScript). Both libraries had to be added independently to a project.
Recently, we took another look at our two massively similar libraries and asked if, with the changes to the Clojure/ClojureScript landscape, we could do better. Not to spoil the fun, but we were able to combine our two libraries into one. Unfortunately it did take a few absolutely-necessary hacks but that is the cost of working with such an exciting, constantly changing technology like ClojureScript. We definitely look forward to the day when ClojureScript becomes an effortless abstraction of JavaScript but until then a little creativity will have to suffice.
Setting Up Your Base Project:
A Sample Base Project is available if you would like to use it. It already has Speclj installed and running for both Clojure and ClojureScript. It also has branches with working code for each part of this tutorial. We will be referencing this project throughout this tutorial.
Regardless of whether you use the sample project, your project structure should look similar to its structure. This is especially true for your src/
and spec/
paths. You will want a structure that resembles src/file-type/project-name/code.file-type
. For example, in the sample project we have src/clj/myproject/core.clj
for Clojure source and src/cljs/myproject/core.cljs
for the ClojureScript source.
A similar structure should be used for your tests.
myproject
|
|-- test
| |-- clj
| | |-- myproject
| | | |-- test-code.clj
| |-- cljs
| |-- myproject
| |-- test-code.cljs
|-- src
|-- clj
| |-- myproject
| |-- source-code.clj
|-- cljs
|-- myproject
|-- source-code.cljs
Dividing Your ClassPaths with Profiles:
The Clojure and ClojureScript versions of your project, although residing within the same jar, will have different classpaths. It is important that you isolate these classpaths in a way that will allow you to test and run the two portions of your library independently. This can be done by adding separate profiles to your project.clj
file. Let’s make :clj
and :cljs
profiles.
Both profiles will use the org.clojure/clojure
and speclj
dependency so they should remain outside the :profile
map. That means that all we need for the :clj
profile is:
{
:source-paths ["src/clj"]
:test-paths ["spec/clj"]
}
For the :cljs
profile, we will need the standard :cljsbuild
information. Additionally, the sample base project uses speclj
for testing so we will see some syntax necessary for Speclj as well.
:cljs {:dependencies [[org.clojure/clojurescript "0.0-2014"]
[org.clojure/tools.reader "0.7.10"]
[lein-cljsbuild "1.0.2"]]
:plugins [[lein-cljsbuild "1.0.2"]]
:cljsbuild ~(let [run-specs ["bin/speclj" "target/tests.js"]]
{:builds {:dev {:source-paths ["src/cljs" "spec/cljs"]
:compiler {:output-to "target/tests.js"
:pretty-print true}
:notify-command run-specs}}
:test-commands {"test" run-specs}})}
Now let make our lives easier by adding testing aliases to our project. Here is the alias to run your Clojure speclj
tests using the :clj
profile:
:aliases {
"clj-test" ["with-profile", "clj", "spec"]
}
The ClojureScript testing alias looks very similar:
:aliases {
"clj-test" ["with-profile", "clj", "spec"]
"cljs-test" ["with-profile", "cljs", "cljsbuild", "test"]
}
Now, if you have Speclj
configured correctly, you can run lein clj-test
and lein cljs-test
from the command line to run your Clojure and ClojureScript tests respectively.
Working With CLJX:
Next, we will use the CLJX library. CLJX translates a .cljx
files into separate .cljs
and .clj
files. You can use small #+cljs
and #+clj
tags to differentiate which forms you would like included in which version. In the 8th Light Speclj project, CLJX replaced our hand-rolled pre-compiler. This gave us the benefit of relying on an open-source, updated dependency instead of our own program.
However, CLJX comes with a few downsides. First, you will have to keep track of the status of your cljx results. If you make a change in a .cljx
file and, for whatever reason, do not recompile the cljx folder, your changes will not appear in your .clj
and .cljs
files. Second, You should be careful to not make changes to the generated .clj
and .cljs
files since they will be overridden the next time you generate your cljx
output. Third, if you are running a test autorunner, it will likely not pick up changes to .cljx files.
So CLJX comes with a cost, but it does allow you to keep a relatively similar code base for your Clojure and ClojureScript libraries.
Adding CLJX to your Project.clj
To add CLJX simply add [com.keminglabs/cljx "0.3.1"]
to your general :dependencies
map. You will then have to configure source and output paths in the :cljx
key:
:cljx {:builds [{:source-paths ["src/cljx"]
:output-path "target/generated/src/clj"
:rules :clj}
{:source-paths ["src/cljx"]
:output-path "target/generated/src/cljs"
:rules :cljs}
{:source-paths ["spec/cljx"]
:output-path "target/generated/spec/clj"
:rules :clj}
{:source-paths ["spec/cljx"]
:output-path "target/generated/spec/cljs"
:rules :cljs}]}
So now every .cljx
file in our src/cljx
folder will be translated to separate .clj
and .cljs
version, which will be stored in the target/generated/src/
folder.
It also will help to add the cljx hooks
so that cljx will automatically build your files when you attempt a normal leiningen command:
:hooks [cljx.hooks]
Configuring Your Source-Paths and Test-Paths
With cljx installed, we will have to change our source-paths
and test-paths
so that they look for the files generated by cljx.
For your :clj
profile, simply modify the paths like below:
{
:source-paths ["src/clj", "target/generated/src/clj"]
:test-paths ["spec/clj", "target/generated/spec/clj"]
}
For your :cljs
profile you new source and test resources will both go in :source-paths
collection:
{
:source-paths ["src/cljs"
"spec/cljs"
"target/generated/src/cljs"
"target/generated/spec/cljs"]
:compiler {:output-to "target/tests.js"
:pretty-print true}
:notify-command run-specs
}
Making Things Easy with Aliases
We're almost done. With cljx hooks
in your project.clj
, cljx will auto-generate files before you run you Clojure tests. However, for ClojureScript we'll have to test leiningen to compile before we run tests. We can do this easily by changing our cljs-test
alias:
:aliases {
"cljs-test" ["do" "cljx" "with-profile" "cljs" "cljsbuild" "test"]
}
We also are little more concerned about the target
directory. So we may want to clean that directory before regenerating our cljx code. We can add a few aliases that help us with that:
{:aliases
{"clj-clean-test" ["do" "clean," "clj-test"]
"cljs-clean-test" ["do" "clean," "cljs-test"]}}
Lastly, we can add a final alias that will run both our clj
and cljs
tests in one command:
:aliases {
"all-tests" ["do" "clean," "clj-test," "cljs-test"]
}
Now our project.clj
file is updated. We should now be able to add a .cljx
file to our project and it will generate separate but similar .clj
and .cljs
files.
Adding a .cljx File to your Project
If you are using the sample project you will see that we already have src/clj/myproject/core.clj
and src/cljs/myproject/core.cljs
. We will create a similar directory structure for the .cljx
files.
Let us make a src/cljx/myproject/
folder and add shared_file.cljx
to the new folder. Next, let us make a spec/cljx/myproject/
folder and add shared_file_spec.cljx
.
In shared_file.cljx
add:
(ns myproject.shared-file)
(defn multiply [x y]
(* x y))
*
In shared_file_spec.cljx
add:
(ns myproject.shared-file-spec
#+clj :require #+cljs :require-macros [speclj.core :refer [describe it should=]]
:require [speclj.core]
[myproject.shared-file :as shared-file])
(describe "sample cljx file"
(it "uses cljx files to generate tested code in clj an cljx"
(should= 12 (shared-file/multiply 3 4))))
As you can see, the spec file includes #+clj
and #+cljs
tags. For this file, that means that cljx will add :require
to the clojure version of the file and :require-macros
to the Clojure version of the file. These tags will include or exclude the entire form that follows them. So one tag can include/exclude an entire function.
As a side note, the :require-macros
key is the way we get access to our Clojure macros in ClojureScript. Since Speclj uses macros for both Clojure and ClojureScript the small #+
tags let us properly import the macros for both platforms.
Run our Tests with A CLJX file
Now that we have cljx set up and a .cljx
file and test file, we can run lein clj-clean-test
and lein cljs-clean-test
. Both should evaluate the multiply
test included in the single .cljx
file in their respective platforms by testing against the cljx
generated code.
Now we have a single .cljx
source and spec file that will be generated into separate .clj
and .cljs
files. And we can test our code in both Clojure and ClojureScript.
Using Platform Files to Isolate Library Differences
sample code through Platform Files
Now that we can write a single file that ultimately becomes separate .clj
and .cljs
files, we can look at how we're going to deal with the differences in the Clojure and ClojureScript platforms.
We will isolate these differences in two files (one .clj
and one .cljs
) with the same file name and same namespace name. We will then reference this common namespace in our code. When our clojure code runs, the .clj
namespace will execute, and when we run our ClojureScript code, our .cljs
namespace will execute. Thus the rest of our files can be written without a need to focus on platform details.
An example will illustrate the project design:
Platform Files: An Example
Let's say we want to use our platform's abs
function to find the absolute value of a number. In Clojure this is done using the org.clojure/math.numeric-tower
library, while ClojureScript would use JavaScript's Math/abs
function.
To set up this example, we'll make a .cljx
function that uses abs
Lets add an "absolute difference" functionality to shared_file.cljx
. This functionality will simply find the absolute difference between two numbers.
As always, we'll start with a test. Add the code below to the describe
block in your shared_file_spec.cljx
:
(it "finds the absolute difference between two numbers"
(should= 1 (shared-file/abs-diff -101 100)))
This test will simply help us decide if everything is working correctly. Now let's focus on the source code.
Add the code below to your shared-file.cljx
:
(ns myproject.shared-file
(:require [myproject.platform :as platform]))
And add your new function, which should look like this:
(defn absolute-difference [x y]
(- (platform/absolute-val x) (platform/absolute-val y)))
As you can see, we're referencing a platform namespace. This namespace will change based off of what platform we're executing. Let's now make our platform namespace files.
Create platform.clj
in src/clj/myproject/
and platform.cljs
in src/cljs/myproject/
In platform.clj
add:
(ns myproject.platform
(:require [clojure.math.numeric-tower :as math]))
(defn abs [num]
(math/abs num))
You'll also have to add the numeric-tower
dependency to your :clj
profile in project.clj
:
:dependencies [[org.clojure/math.numeric-tower "0.0.4"]]
Now let's move to the cljs side. In platform.cljs
add:
(ns myproject.platform)
(defn abs [num]
(js/Math.abs num))
Now we have two like-named functions in two like-named namespaces. If we run our tests, they'll pass. This works because when Clojure runs, the platform.clj
file will be used and the numeric-tower
library will be executed. When ClojureScript runs, the platform.cljs
file will be used and JavaScript's abs
function will be executed. Our shared-file
doesn't need to know about those details. It can simply call the platform namespace's function and receive the results.
We now have a function, abs-diff
that is written just once yet can be used for both Clojure and ClojureScript. This means that not only can we write a single code base that can run in Clojure and ClojureScript, it also means that the differences between the two platforms is isolated to the platform
namespace.
Using Platform Files with Macros
sample code through Platform Files with Macros
In the last section we used separate .clj
and .cljs
files with the same namespace name to isolate the platform differences between Clojure and ClojureScript. But what if we want to use these files in macros? You can still use these files in macros but there are a few tweaks needed to our previous platform strategy.
The issue with macros is that there will be no equivalent .cljs
macro file. Most macros will stay on the Clojure side of your project. Let's take a look at an example.
Platform Files and Macros: An Example
First, we need to tweak your platform.clj
file. Add the snippet below to your :cljs
profile. That way your Clojure macro will be available to your ClojureScript profile.
:source-paths ["src/clj"]
Now let's add a new test to our shared_file.cljx
file which will test a simple macro.
Create a macro.clj
file in your src/clj/myproject/
folder. You can leave it empty for now.
Next, add the dependency to your shared_file_spec.cljx
under :require-macros
. Your namespace should now look like this:
(ns myproject.shared-file-spec
#+clj :require #+cljs :require-macros
[speclj.core :refer [describe it should=]]
[myproject.macros :as macros])
(:require [speclj.core]
[myproject.shared-file :as shared-file])
For this example, we'll add our existing platform/abs
functionality to a macro. But tests come first! Add a new test that will evaluate an abs
function located in your macro file:
(it "finds absolute value using macro"
(should= 2 (macros/abs -2)))
So now we have a test but it won't pass since we have nothing in our macro.clj
file
Let's now add a namespace and simple abs
function to macro.clj
.
(ns myproject.macros
;(:require [myproject.platform]) ;uncommenting will break cljs tests
)
(defmacro abs [x]
`(myproject.platform/abs ~x))
Now here is where things get interesting. As you can see we've commented out the :require
statement and our macro function uses a fully qualified namespace. It seems like our macro might not be able to find the platform namespace. But let's run our tests.
They should pass!
Now uncomment the :require
statement and rerun your tests. Your ClojureScript tests will fail to run!
It seems like we should :require
our platform namespace since the file uses it, but this will actually break the ClojureScript-side tests. This is because the macro file will always attempt to evaluate the .clj
version of our platform file if it is defined in the :require
namespace. By using a fully-qualified namespace in our macro instead of referencing it through a :require
statement, the correct platform file will be evaluated at macro expansion. So, when it comes to macros, you should not :require
the platform file but instead use its fully-qualified namespace where it is needed.
Now we've seen how to use platform files to isolate platform difference in both normal functions and clojure macros. These platform files can get you far, but they don't get you all the way there. In the next part of the tutorial we'll see how to use an ugly but effective "if" statement to get essentially complete cross-platform functionality.
Powerful but Perilous Context-aware Macros
sample code through Context-aware Macros
In the previous portion of this tutorial, we were able to get a great deal of cross-platform functionality using platform files. But now we'll look at a way to define a clojure macro with platform-specific code, using a very fragile if statement.
First, let's look at an example:
Context-aware Macros: An Example
In your shared_file_spec.cljx
files add this test:
(it "catch slurp failure"
(should= true (macros/slurpable-file? "badfilename")))
In this test, we're attempting to slurp
a bad file name. In both Clojure and ClojureScript, this will raise an exception. But exceptions are a little different in Java and JavaScript. Java will require an Exception
while JavaScript will use a js/Ojbect
. You can see the [Clojure documentation][clojure_documentation] for an example of both situations.
Let's go to our macros.clj
file and see what we can do to pass this test for both platforms.
Here's what the Clojure version might look like. But of course it won't work in ClojureScript. ClojureScript won't know what to do with Exception
.
(defmacro slurpable-file? [file-name]
`(try
(slurp ~file-name)
(catch Exception e# true)))
Here's what the ClojureScript version might look like. But of course it won't work in Clojure. Clojure won't know what to do with js/Ojbect
.
(defmacro slurpable-file? [file-name]
`(try
(slurp ~file-name)
(catch js/Object e# true)))
Finding the Context
We have two separate macros that simply won't work on both platforms. But what if we could determine if the macro.clj
file was expanding in a Clojure context or a ClojureScript context. Maybe then we could use this information to build the correct macro for the currently-executing library.
This is where a fragile "if" statement comes into play. It looks like this:
(defn cljs? []
(boolean (find-ns 'cljs.analyzer)))
As you can see it determines if the file is running in a ClojureScript context if the cljs.analyzer
namespace can be found. Otherwise, we will assume it is in a Clojure context. We can use this function to create a macro that can combine our two previous macros.
(defmacro slurpable-file? [file-name]
`(try
(slurp ~file-name)
~(if (cljs?)
'(catch js/Object e# true)
'(catch Exception e# true))))
If we try this macro, it will pass the tests for both platforms. It does this by adding the correct platform-specific catch statement during macro expansion. Thus we have a macro that, to some degree, is away of the context of its expansion. This may seem like it has opened up an amazing set of functionality but the if statement is fragile. It relies on the existence (or lack thereof) of a ClojureScript specific namespace. If something changed in ClojureScript, the entire library could fail. Thus using the cljs
function above is a bit of hack. But it gets us where we want to go and there are few other options.
ns-resolve Can Help Too
As a brief side note, the hack noted above will still fail if the Clojure compiler cannot recognize the name of a currently absent ClojureScript namespace. An example is cljs.compiler/munge
. If this included in a .clj
macro, your Clojure-side tests will fails because Clojure will not find the namespace.
However we can get around this using ns-resolve
. Instead of referencing cljs.compiler/munge
we can replace it with:
1 (ns-reolve 'cljs.compiler "munge")
This will essentially push the cljs.compiler
namespace check to runtime. If you have your macro setup correctly, the cljs
namespace should never be called in Clojure.
So now we've seen how to use platform files to isolate platform-specific code and we've also seen a little hack that can help when macros must be defined in a platform-specific manner. In the next part of this tutorial we'll put it all together, quite literally, and combine our Clojure and ClojureScript libraries into a single jar.
Adding clj and cljs Code to a Single Jar
sample code through Single Jar
In the previous parts of this tutorial we've built a code base that can deliver the same functionality in both Clojure and ClojureScript. Now we'll see how we can deploy this functionality in one Jar. This gives others the ability to import one library and gain both your clj
and cljs
functionality.
Let's go to our project.clj
file. We'll add an entirely new profile called "combined"
:profiles {
:combined {
:dependencies [[org.clojure/math.numeric-tower "0.0.4"]
[org.clojure/clojurescript "0.0-2014"]
[org.clojure/tools.reader "0.7.10"]
[lein-cljsbuild "1.0.2"]]
:source-paths ["src/clj", "target/generated/src/clj"]
:resource-paths ["src/cljs", "target/generated/src/cljs"]
:test-paths ["spec/clj", "target/generated/spec/clj"]
:cljsbuild ~(let [run-specs ["bin/speclj" "target/tests.js"]]
{:builds {:dev {:source-paths ["src/cljs"
"spec/cljs"
"target/generated/src/cljs"
"target/generated/spec/cljs"]
:compiler {:output-to "target/tests.js"
:pretty-print true}}}
:test-commands {"test" run-specs}})
}
}
This profile effectively combines our :clj
and :cljs
profiles into one. You'll note that all of the dependencies or both profiles are added to the :combined
profile.
We've also added :resource-paths
with src/cljs
and target/generated/src/cljs
. This will add our ClojureScript files to our jar, while the normal :source-paths
will add our Clojure code.
So let's test this new, combined profile. Here, an alias will be helpful:
"combined-tests" ["do" "clean," "with-profile" "combined" "spec," "with-profile" "combined" "cljsbuild" "test"]
Now if we run lein combined-tests
everything should pass!
If you lein with-profile combined jar
and go to your target/
file, you can jar tf
the generated .jar
file and see how both the .clj
and .cljs
file are combined into the same library.
All we have to do now is install our project using the combined
profile. Again we'll use an alias to make things easy:
"install" ["do" "clean," "with-profile" "combined" "install"]
Now we can lein install
and we will have a single library that will work for both Clojure and ClojureScript.
That's the end of the tutorial. I hope you enjoyed it!