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.
{:builds
{:app
{:target :browser
:output-dir "public/js"
:modules
{:main {:init-fn my.app/init}}}
:js-options
{: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
(ns my.app
(: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.
Conclusion
: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.