How about webpack now?

A while ago I wrote about why shadow-cljs doesn’t use webpack. All of that still applies and shadow-cljs still works completely without webpack (or any other JS tools) and will continue to do so. The recent release of :target :bundle support in ClojureScript itself however has people asking how that affects shadow-cljs?

TL;DR: It doesn’t.

What is :target :bundle?

You should read the official guide for the complete story but in short the :bundle will produce one output .js file in a format where the JS tool of your choice will further process it and provide the JS dependencies instead of the CLJS compiler trying to do this. Most commonly that will likely be done by webpack but CLJS has no further stake in which JS tool you actually use to process this. The only thing that needs to happen is that the output bundle file is processed by something before it is being loaded. As a convenience CLJS provides a :bundle-cmd setting to allow you configure to run a custom command but you can just run it yourself as well.

As I described in my previous post that is basically the first approach I tried in shadow-cljs but then abandoned for the reasons I mentioned. It is great to see a better default story in CLJS for this but it doesn’t solve the problems I wanted to solve.

What if I want to use <my favorite JS tool>?

There might be reasons why you’d rather stick with a JS tool to provide the JS dependencies you need. The default is to let shadow-cljs do everything so you don’t have to worry about it but if you really want to you have a couple options. For example webpack provides many features that shadow-cljs does not cover.

Option #1: :target :npm-module

:npm-module was the first option I added way back when. Its intent is to compile CLJS to an output format that can directly be consumed by any other JS tool (eg. node, webpack, etc). It allows easily integrating CLJS code into an existing JS codebase. I’m terrible at naming things and I should have called it :target :commonjs since that would be more accurate.

Basically each CLJS namespace will be exposed as a separate .js file that can be directly required from JS (eg. in node without any other additional tool at all). As fas as the JS tool is concerned the code will look like any other npm library.

:npm-module however is a rather hacky way so it is really only useful in situation where you want integrate CLJS into an existing JS project.:npm-module works well enough though but it should not be used if you just have a couple npm depdencies you need to fill.

Option #2: :js-provider :external

shadow-cljs has the concept of a :js-provider built-in. This controls who is actually in charge of providing JS dependencies. For node builds this is just :require which maps all JS requires in your ns forms to regular node require calls. For :browser builds it defaults to :shadow which means shadow-cljs will provide all JS dependencies and bundle them for you. An additional :js-provider I added not too long ago is :external. It is similar to what :bundle from CLJS provides but with few different design choices.

  {:target :browser
   :output-dir "public/js"
   {:main {:init-fn}}}
   {:js-provider :external
    :external-index "target/index.js"}}}

In this example shadow-cljs will generate all the regular CLJS output and write it into the public/js directory. It will however not process any JS dependencies itself and instead just generates the additional :external-index file at the specified location. That file is just a regular JS file and will contain all the JS require calls that your CLJS code used and a bit of glue code that exposes them so that the CLJS code can find them at runtime.

Supposed you have

  (:require ["react" :as react]))

The generated index file will contain require("react") which JS tools understand. You are responsible for further processing that file and making sure that the output of that is loaded before the CLJS output.

So you could for example run

npx webpack --entry target/index.js --output public/js/libs.js

and then include the generated libs.js from webpack and the generated main.js from shadow-cljs in your HTML.

<script defer src="/js/libs.js"></script>
<script defer src="/js/main.js"></script>

This is basically an automated version of the double-bundle approach that a few people have been using for a while.

However this is different in that the output is intended to stay separate. JS code lives in one and CLJS code in the other. JS code can’t interact with the CLJS code but CLJS code can access the provided JS dependencies. This does give you a very basic code-splitting out of the box which is a good default IMHO. However as mentiond in my previous post this kind of code-splitting is very limited and not as fine grained as what :js-provider :shadow will give you. You can still use :modules for your CLJS code but your external JS file might get unwieldly large and not fit your :modules properly as JS dependencies won’t be split at all.


:bundle is a good step forward for CLJS in general as good npm support is no longer limited to shadow-cljs. Previously working with CLJSJS packages or manually setting up the “double-bundle” configs could get quite complicated and brittle in larger projects so I’m happy to see this disappear. :bundle still leaves a lot of things unsolved and we’ll see how that evolves over time.

shadow-cljs will indirectly benefit from this when more CLJS libraries move to direct npm package dependencies and away from CLJSJS packages. :bundle may even get more people to try shadow-cljs since switching between :bundle with regular CLJS and shadow-cljs should not require any code-changes whatsoever. Previously that was not the case for projects that had complicated “double-boundle” setups or used many CLJSJS packages.

Beyond that :bundle does not affect anything in shadow-cljs at all. You can just let shadow-cljs continue to do everything for you, without having to worry about setting up other tools. You always have the other options at your disposal if you really want to.


Add a Comment