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.
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 sepecify 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
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 multile vars. For this reason, the webpack conig uses a special entry point,
With this setup, adding a new NPM dependency is as easy running
yarn add react-datatime, 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/reactdependency, we also lose the
cljsjs.reactnamespace. 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.
Using the double bundle approach is powerful, as it taps into the ecosystem and accumulated wisdom of the NPM community.