Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Let's Build a Bitbucket Add-on in Clojure! - Part 6: Finally, ClojureScript!

DZone's Guide to

Let's Build a Bitbucket Add-on in Clojure! - Part 6: Finally, ClojureScript!

Here's part six of our building a Bitbucket add-on in Clojure series! In this chapter, we'll use ClojureScript, revisit our Connect JavaScript, restructure our project, and more!

· Java Zone
Free Resource

Managing a MongoDB deployment? Take a load off and live migrate to MongoDB Atlas, the official automated service, with little to no downtime.

Back in part 4 of this series we introduced two methods of accessing Bitbucket from our Atlassian Connect add-on, including via the client-side (browser) JavaScript API. However as we've written all of our server-side code in Clojure so far, it's a shame to have to switch to another language. In this installment, we'll take a look at ClojureScript and how we can integrate it into our project.

Revisiting our Connect JavaScript

In part 4 we introduced accessing the Bitbucket API via the JavaScript API. This API uses the cross-domain messaging standard to open a communication channel between the parent (i.e. Bitbucket) page and the iFrame child page (i.e. embedded Connect add-on content). This API uses a special AP.require() call to have the parent page make asynchronous calls on behalf of the child iFrame. We provided some examples of using this API in part 4 of this series:

// Bitbucket Connect also supports a client side library - AP (for "Atlassian Plugins") - that
// allows you to interact with the host application. For example, you can make authenticated
// requests to the Bitbucket REST API ...

AP.require('request', function(request) {
    request({
        url: '/1.0/user/',
        success: function(data) {
            $('#displayName')
                .text(data.user.display_name)
                .next('.loading').hide();
        }
    });
});

// ... and set cookies (browser security policies often prevent this from being done in iframes).

var COOKIE_NAME = 'example-visits';

AP.require('cookie', function(cookie) {
    cookie.read(COOKIE_NAME, function(visits) {
        visits = (visits ? parseInt(visits) : 0) + 1;
        cookie.save(COOKIE_NAME, visits, 30);
        $('#pageVisits')
            .text(visits)
            .next('.loading').hide();
    });
});

However as we've written our add-on in pure Clojure so far it seems a shame not go the whole way. Enter ClojureScript...

So What is ClojureScript?

ClojureScript is a version of Clojure that targets JavaScript and Google Closure. This allows developers to write a major subset of Clojure that will run in the browser. This brings a number of advantages such as immutable/persistent data structures, macros, and access to channel-based concurrency via core.async (the benefits of which we'll get into in a later installment). So let's port our JavaScript which interfaces with the Bitbucket API to ClojureScript...

Restructuring Our Project

But the first thing we'll want to do is restructure our project a little. By default Leiningen places all Clojure code into src/. However we'll now be working with two compilers (one for Clojure and another for ClojureScript), so it's good practice to separate the two codebases, so we'll move them into separate subdirectories:

[ssmith:~/projects/hello-connect] $ mkdir -p src/clojure/
[ssmith:~/projects/hello-connect] $ mv src/hello_connect/ src/clojure/
[ssmith:~/projects/hello-connect] $ mkdir -p src/cljs/hello_connect/

Now we have src/clojure and src/cljs; we just need to tell Leiningen about this change in our project.clj:

:source-paths ["src" "src/clojure"]
:test-paths   ["test" "test/clojure"]

Translating From JS to CLJS

Now that we have a home for our ClojureScript module, let's produce a straight port of Tim Pettersen'sexample JavaScript:

;; Bitbucket Connect also supports a client side library - AP (for "Atlassian Plugins") - that
;; allows you to interact with the host application. For example, you can make authenticated
;; requests to the Bitbucket REST API ...

(ns hello_connect.core
  (:require [cljs.reader :as reader]
            [goog.dom :as dom]
            [goog.style :as style]))

(enable-console-print!)

(defn set-name [data]
  (let [name (((js->clj data) "user") "display_name")]

    (-> (dom/getElement "displayName")
        (dom/setTextContent name))
    (-> (dom/getElement "nloading")
        (style/setElementShown false))))

(.require js/AP "request"
          (fn [request]
            (request (clj->js
                      {"url" "/1.0/user/"
                       "success" set-name}))))

;; ... and set cookies (browser security policies often prevent this from being done in iframes).

(def cookie-name "example-visits")

(defn set-count [count]
  (-> (dom/getElement "pageVisits")
      (dom/setTextContent count))
  (-> (dom/getElement "cloading")
      (style/setElementShown false)))

(.require js/AP "cookie"
          (fn [cookie]
            (.read cookie cookie-name (fn [visits]
                                        (let [n (inc (reader/read-string visits))]
                                          (.save cookie cookie-name n 30)
                                          (set-count n))))))

As you can see above, this looks like a mixture of Clojure and JavaScript. The first thing we do is import some of the Closure and Clojure utilities, and then define some functions to use these libraries to manipulate the DOM and set our values. Then we make two calls to the Bitbucket AP.require() API with callbacks to lookup the values. Which is to say our code functions almost exactly the same as the original. The only major difference is that we're using Google's Closure library to manipulate the DOM instead of jQuery.

Building ClojureScript

We put this code into src/cljs/hello_connect/core.cljs and now we need to compile it. Naturally Leiningen has a plugin to help with this, so let's add it to the :plugins section:

[lein-cljsbuild "1.1.1"]

We also need to add ClojureScript itself to the :dependencies section:

[org.clojure/clojurescript "1.7.48"]

(As always you can use lein ancient to keep these dependencies up to date.)

Now we need to configure the compiler itself, which has some trade-offs and may require some explanation...

Understanding the Clojure/Closure Compiler

The Google Closure compiler has a number of optimisation levels that have different effects on the code-base. The main two we're interested in are :none, which is recommended for development, and :advanced which is recommended for production. However :none requires the developer to provide some dependencies manually which are not required in production (as :advanced does more packing and minimisation); this effectively means you must use slightly different HTML in dev, which is not a great idea. It is usually considered bad practice to make a lot of changes moving from dev to production. On the other hand the :advanced optimisations are really slow (11 second compilation time for our simple ClojureScript code). However like Clojure/Leiningen's slow start-up time not being an issue in practice because we always leave the REPL running, in practice we don't rebuild ClojureScript from scratch. While ClojureScript supports REPL development, another option is to use its incremental compilation. Basically all we need to do is run lein cljsbuild auto and not only will it compile the code but it will watch the source files and initiate a conditional compilation on change, which is much faster:

[ssmith:~/projects/hello-connect] $ lein cljsbuild auto
Watching for changes before compiling ClojureScript...
Compiling "target/cljs/public/cljs/core.js" from ["src/cljs"]...
Successfully compiled "target/cljs/public/cljs/core.js" in 10.46 seconds.
Compiling "target/cljs/public/cljs/core.js" from ["src/cljs"]...
Successfully compiled "target/cljs/public/cljs/core.js" in 2.489 seconds.

And for more advanced development scenarios there is also Figwheel, which will also live-load ClojureScript into a running browser window.

So we want to build our ClojureScript with :advanced optimisations, however, there is one gotcha in doing that. :advanced is very aggressive and will mangle any function names, including functions that are external to our code; in this case the AP.require()/(.require js/AP) calls. There a couple of ways to fix this, but the least intrusive is to just declare any 'protected' symbols to the compiler. This is done by adding them to a file on the path, in our case under resources/AP-externs.js:

var AP = function() {}
AP.require = function(method, func) {}

This declares that the AP module and require() functions should not be mangled.

Building ClojureScript (Again)

Now we can give Leiningen the information it needs to compile our code in our project.clj:

:cljsbuild {:builds
            [{:source-paths ["src/cljs"]
              :compiler {:output-to "target/cljs/public/cljs/core.js"
                         :externs ["AP-externs.js"]
                         :optimizations :advanced}}]}

This compiles our code into a single file core.js if we issue lein cljsbuild auto. We'd also like to include this code when building our uberjar, so we add the output directory into the resources path so it gets merged with all our other HTML, CSS, etc:

:resource-paths ["resources"
                 "target/cljs"]

Of course, we also need to update our HTML to use our generated JavaScript. Open our template connect-example.selmer and change the line:

<script src="/js/addon.js"></script>

to:

<script src="/cljs/core.js"></script>

The last thing we need to do is tell Leiningen that we would like our cljsbuild called as part of any builds it performs:

:hooks [leiningen.cljsbuild]

And that's it. We can now build and run our uberjar as we did in part 5, and we can see our ClojureScript running in the browsers.

Next Time

We've produced a lot of boilerplate code over these 6 parts of our tutorial. But boilerplate isn't something that fits into the Clojure world; if something is repetitive then it should be automated away. In the next installments, we're going to have a look at some of the ways Clojure and its ecosystem enables this.

The Code

The code for this part of the tutorial series is available in the part-6 tag in the accompanying hello-connect Bitbucket repository. There will also be code appearing there for the later parts as I work on them if you want to skip ahead.

MongoDB Atlas is the easiest way to run the fastest-growing database for modern applications — no installation, setup, or configuration required. Easily live migrate an existing workload or start with 512MB of storage for free.

Topics:
bitbucket ,clojure

Published at DZone with permission of Ian Buchanan, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}