ClojureScript with Webpack

This guide requires ClojureScript 1.10.741 or later and assumes familiarity with the Quick Start.

This page documents how to integrate ClojureScript with a typical JavaScript bundler such as Webpack. You should have Node.js installed. This guide assumes you have read through the Quick Start. This guide borrows liberally from this excellent guide on Webpack 2.

While we happen to use Webpack here, these instructions are easily adapted to other bundlers like Metro for React Native.

Setting Up Your Project

First create a project directory:

mkdir hello-bundler
cd hello-bundler

Create a deps.edn file the following contents:

{:deps {org.clojure/clojurescript {:mvn/version "1.10.741"}}}

Create an empty package.json file:

echo "{}" > package.json

Add webpack and its command line tools:

npm install --save-dev webpack webpack-cli

We’re now ready to setup our JS dependencies.

JavaScript Dependencies

Install react and react-dom:

npm install --save react react-dom

Now create src/hello_bundler/core.cljs with the following contents:

(ns hello-bundler.core
  (:require [react]))

(.log js/console react/Component)

Notice that we are requiring React as if it was a normal require and a normal namespace.

In order for this to work we need to set a couple of compiler options. Create a build.edn file with the following:

{:main hello-bundler.core
 :output-to "out/index.js"
 :output-dir "out"
 :target :bundle
 :bundle-cmd {:none ["npx" "webpack" "./out/index.js" "-o" "out" "--mode=development"]
              :default ["npx" "webpack" "./out/index.js" "-o" "out"]}
 :closure-defines {cljs.core/*global* "window"}} ;; needed for advanced

Our build will generate out/index.js which is exactly the entry file that Webpack is looking for. We’ll write the bundler result back into the output directory.

Note the new :target :bundle option. This ensures that the generated code is compatible with popular JavaScript bundlers that can handle Node.js style require. It also sets a bunch of other sensible defaults like externs inference, so that advanced compilation will just work. :bundle-cmd is just an arbitrary shell command to run after the ClojureScript build completes. In the case of watching bundlers like Metro you probably won’t bother with :bundle-cmd.

Let’s see this in action, the following will build your project then start a REPL:

clj -M -m cljs.main -co build.edn -v -c -r

Your default browser will open http://localhost:9000. Open the Developer Console, you should see that React.Component got logged.

At the REPL you can require react and interact with it:

user> (require 'react)

Overriding a Foreign Library

You may find that you want to use React from node_modules perhaps because the latest bundled version hasn’t yet appeared on CLJSJS. Yet, you still want to use some ClojureScript React binding like Reagent. ClojureScript supports this out of the box.

To demonstrate this, change your deps.edn to the following:

{:deps {org.clojure/clojurescript {:mvn/version "1.10.741"}
        reagent {:mvn/version "0.10.0" :exclusions [cljsjs/react cljsjs/react-dom]}}}

Change your source file to the following:

(ns hello-bundler.core
  (:require [goog.dom :as gdom]
            [reagent.dom :as dom]))

(defn simple-component []
   [:p "I am a component!"]
    "I have " [:strong "bold"]
    [:span {:style {:color "red"}} " and red "] "text."]])

(dom/render [simple-component] (gdom/getElement "app"))

Rebuild your project, run the REPL:

clj -M -m cljs.main -co build.edn -v -c -r

To verify that externs inference allows advanced compilation to work, let’s make an advanced build. REPLs don’t work under advanced compilation so you’ll have to manually open http://localhost:9000:

clj -M -m cljs.main -co build.edn -O advanced -v -c -s

That’s it!

Building Webworkers

When building webworkers with :target :bundle, you use webpack (or your preferred bundler) to add the webworker bootstrap.

So your build’s :target will still be :bundle (not :webworker), but you will tell your bundler to build a webworker. For example, with webpack you add the --target=webworker argument to your :bundle-cmd entries.

You also need to define cljs.core/global as "self" (as opposed to "window" in browser builds).

An example build-webworker.edn might look like:

{:main hello-bundler.webworker
 :output-to "out/worker/index.js"
 :output-dir "out/worker"
 :target :bundle
 :bundle-cmd {:none ["npx" "webpack" "out/worker/index.js" "-o" "out/worker/main.js" "--target=webworker" "--mode=development"]
              :default ["npx" "webpack" "out/worker/index.js" "-o" "out/worker/main.js" "--target=webworker"]}
 :closure-defines {cljs.core/*global* "self"}} ;; needed for advanced

Original author: David Nolen