published Jun 06, 2017
Lately the Clojuresphere has been abuzz with efforts to make it easy to integrate NPM dependencies in ClojureScript projects. The reason is hardly a mystery. The NPM package repository, warts and all, contains a large amount of high-quality libraries. Access to these libraries instantly gives you leverage.
NPM’s original home is server-side development. These days access to NPM is important even - perhaps especially - when your ClojureScript code is targeting the web browser. Toolkits like Reagent, Rum and Om make it easy, if not trivial, to include pre-baked Javascript React components in your ClojureScript projects. Access to battle-tested components like react-select, react-datetime or frameworks like reactstrap can significantly accelerate frontend development and keeps the incidental complexity of working with the DOM at bay.
So what’s the best way to integrate 3rd party Javascript in a ClojureScript project? The default answer is to integrate these dependencies using Maven coordinates from CLJSJS. CLJSJS is a wonderful community effort to re-package common Javascript (and React) libraries in Jar for, for consumption by leiningen or boot. Although CLJSJS is fantastic at what it does, in this post I will describe a different way, which I’ll call the “double bundle” approach.
Modern websites usually include their code in the form of a bundle,
essentially a concatenation of individual libraries and modules along
with some glue code. For ClojureScript, the Google Closure compiler is
responsible for generating this bundle. Requiring a cljsjs namespace
(like cljsjs.react
) causes the React.js to be included in
the output bundle. Because the library is included as a foreign lib,
Closure will not attempt to minify or shrink its contents.
Essentially a convenient bridge between the Maven and NPM world, CLJSJS allows you to specify all your dependencies in a single file (project.clj or build.boot). However, the approach also has drawbacks:
Of course CLJSJS packages aren’t magic, so how do they work?
Essentially what they do is to build a single bundle containing the
library, including all its internal dependencies. The bundle’s outgoing
interface is to expose a global var - for react,
window.React
, for react-datetime
window.ReactDatetime
. On the inbound side, the library can
also be a consumer of external dependencies. For example, react-datetime
assumes the existence of a preloaded window.React
object.
Many if not all CLJSJS libraries consist of a bundle built using the
webpack packager. Webpack is a powerful tool in the Javascript world,
perhaps the most popular of its sort (the others in the motely crew are
browserify, rollup, Google Closure, and the react-native packager). The
packages relies on a webpack.config.js
that defines both inbound (“external”) and outbound (“output”)
dependencies. The result is a bundle.js
that defines a
single global object.
This raises the question - why not rely on webpack to orchestrate NPM dependencies altogether? This is the strategy I will propose in the remainder of this post.
The double bundle example projects demonstrates how to use webpack directly to use NPM dependencies in your Clojurescript project. The net effect is that the project’s index.html references two separate bundles, one built by webpack and one built by the Clojurescript compiler and Google Closure compiler combo.
Webpack supports exporting a single var, but we actually need to
export multiple libraries as multiple vars. For this reason, the webpack
conig uses a special entry point, library.js
. For each
dependency that needs to be visible from Clojurescript, the file
contains a line assigning the required module to a global Javascript
variable. This is not elegant, but the effect is similar to how CLJSJS
works: all dependencies are available as global names,
e.g. js/window.React
or
js/window.ReactDOM
.
With this setup, adding a new NPM dependency is as easy running
yarn add react-datetime
, adding
window.ReactDatetime = require("react-datetime")
to
library.js
and rebuilding the bundle using
yarn build
. Having done this, you can access the library
using (goog.object/get js/window "ReactDatetime")
.
What’s more, any library available on NPM should work. Inter-library dependencies will be handled automatically by webpack. Because webpack is popular, chances are getting things to work will require little additional work - and problems will be easy to google.
With the double-bundle approach, a few things need to be kept in mind:
cljsjs/react
dependency, we also lose
the cljsjs.react
namespace. However, libraries like Reagent
require this namespace. The solution is to mock out this namespace with
an empty cljs file. This is admittedly somewhat inelegant.This is presumably for side-effects, a blog by Paulus Esterhazy. Don't forget to say hello on twitter or by email