Npm usage in Figwheel has changed significantly. For reference purposes the original version of this document can be found here
These instructions require
org.clojure/clojurescript
>=1.10.773
andcom.bhauman/figwheel-main
>=0.2.6
.
Read the rest of this document and come back here for a quick reference.
Set the :target
compiler
option
to :bundle
. This will cause the compiler to emit an output file
file that can be bundled a JavaScript bundler like webpack
.
Optionally set the :bundle-cmd
compiler
option
to
{:none ["npx" "webpack" "--mode=development" :output-to "-o"
:final-output-to]}
to ensure the output file is bundled after a compile. Figwheel will
fill in :output-to
and :final-output-to
.
Your host page will need to load the final bundled asset.
You will want to make sure that the
:output-to
file is in the:output-dir
directory so the bundler can resolve the assets it requires.
Relevant Figwheel Options
NPM is a package repository for the JavaScript ecosystem. Almost all available JavaScript libraries are packaged, stored, and retrieved via NPM.
We want to use these libraries inside our ClojureScript codebase, but there is some friction because ClojureScript embraced the Google Closure Compiler and its method of declaring libraries, which is quite different than NPM's.
{::comment} We could get into a debate about why ClojureScript designers decided to embrace the less popular ecosystem, but that is largely academic at this point. I will say that the advantages of effortless interactive development via hot-reloading and the amazing capabilities of the Google Closure Compiler's advanced mode are direct benefits of using the GCC's (Google Closure Compiler's) method of defining modules via simple JavaScript object literals. In other words, without the GCC there would most likely not be a Figwheel.
Nevertheless, experiencing friction while importing libraries from the dominant JavaScript ecosystem is a very unfortunate trade-off. {:/comment}
However, with recent changes in the ClojureScript compiler (along with changes in Figwheel) it is now becoming much more straightforward to include NPM modules in your codebase.
We are going to utilize a JavaScript bundler like Webpack to bundle up the output of our ClojureScript compiled code to produce a final bundled output file which will contain all the NPM libraries we have required in our ClojureScript code.
So the code is going to go through two steps:
It's important to remember that the under the :bundle
target the
output of the ClojureScript compiler is not loadable by the browser it
has to be bundled first.
During development under optimizations :none
the bundled output file
will only contain the NPM libraries and small amount of ClojureScript
boot code that will in turn load our compiled ClojureScript code. So
there exists a bundle file and then a bunch of individually compiled
ClojureScript namespaces that the bundled boot script will need to
load.
It's important to remember that during development the bundled file still depends on the other ClojureScript output files and will not work on its own.
When we deploy to production we will be compiling a single
ClojureScript artifact using :simple
or :advanced
optimizations. This single ClojureScript artifact will then be bundled
along with its NPM dependencies into a single bundle that can be
deployed on its own.
We are going to assume you are starting from the base example.
I'm going to use npm
for this example but if you prefer yarn
go
ahead and use that. It doesn't really matter for this.
There are four steps that we are going to follow to add some libraries to
our hello-world.core
project.
package.json
fileWe will need to initialize npm
for our hello-world
project.
One way to do this is to use the npm init
command. You can of course
just create the file from scratch but npm init
is faster.
Make sure you are in the root directory of the project and execute:
$ npm init -y
This will create a package.json
file in the root directory of your
project along side your dev.cljs.edn
file.
We will need webpack to bundle our application along with our moment
dependency to make it available to our ClojureScript code.
Install webpack
and webpack-cli
:
$ npm add --save-dev webpack webpack-cli
This add webpack
and webpack-cli
as development dependencies in
your package.json
. It will also download them to a node_modules
directory in your project.
Let's say we want to use the moment
library in our
application.
We'll use npm
to add a moment
dependency to our package.json
file in the usual manner:
$ npm add moment
This should download and install the moment
library along with its
dependencies if it has any.
Everything we have done up until this point is very similar to what we would normally do if we were using NPM and Webpack for a simple JavaScript project.
In the dev.cljs.edn
file we'll add the following config:
{:main hello-world.core
:target :bundle
:bundle-cmd {:none ["npx" "webpack" "--mode=development" :output-to "-o" :final-output-to]}}
Understanding the above configuration is important, so I'm going to explain each part.
The :target
compiler
option
is set to :bundle
to instruct the ClojureScript compiler to produce
an output file that can be bundled by a JavaScript bundler like Webpack.
The :bundle-cmd
compiler
option
is set to
{:none ["npx" "webpack" "--mode=development" :output-to "-o" :final-output-to]}}
This provides the ClojureScript compiler with a command that it can use to bundle the intermediate output of the compiler into its final bundled form.
Figwheel adds some additional functionality to the :bundle-cmd
. It
interpolates the keywords :output-to
and :final-output-to
into the
command. In this case the :output-to
is going to be replaced by the
default :output-to
path target/public/cljs-out/dev/main.js
. The
:final-ouput-to
is replaced by the default value of :output-to
with a _bundle
added before the extension or
target/public/cljs-out/dev/main_bundle.js
.
Or stated more simply in this case:
:output-to
is replaced with target/public/cljs-out/dev/main.js
:final-output-to
is replaced with target/public/cljs-out/dev/main_bundle.js
If you supply your own :output-to
cljs compiler option, it will be
used instead of the default.
Thus after the ClojureScript compiler is finished compiling it will
call the :bundle-cmd
to bundle up the output.
In this case it will call:
$ npx webpack --mode=development target/public/cljs-out/dev/main.js -o target/public/cljs-out/dev/main_bundle.js
This will bundle up the main.js
file and pull in the moment
dependency along with it.
Now let's modify our source file to use moment
so that we can make
sure that things are working.
Edit the src/hello_world/core.cljs
to look like:
(ns hello-world.core
(:require [moment]))
(js/console.log moment)
(println (str "Hello there it's "
(.format (moment) "dddd")))
OK now that we've setup everything up we can run the build.
$ clojure -m figwheel.main -b dev -r
The browser and REPL should launch as usual:
Except now you can see one additional line in the output which notifies us that the bundle command was called.
And if you look at the dev tools console of the browser window that
just popped open you should see similar output printed as below
verifying that we were able to use moment
successfully.
Well we successfully used an NPM package from our ClojureScript
code. Now you can npm add
other JavaScript NPM packages and use them
from ClojureScript
:auto-bundle
If you just need to get up and running quickly the :auto-bundle
Figwheel option will set up all the default options that we configured above.
So this configuration is equivalent to the above configuration:
^{:auto-bundle :webpack}
{:main hello-world.core}
When enabled :auto-bundle
will set :target
to :bundle
.
When choosing :webpack
it will set :bundle-cmd
to:
{:none ["npx" "webpack" "--mode=development" :output-to "-o" :final-output-to]
:default ["npx" "webpack" "--mode=production" :output-to "-o" :final-output-to]}
and when choosing :parcel
it will set :bundle-cmd
to:
{:none ["npx" "parcel" "build" :output-to
"--out-dir" :final-output-dir
"--out-file" :final-output-filename
"--no-minify"]
:default ["npx" "parcel" "build" :output-to
"--out-dir" :final-output-dir
"--out-file" :final-output-filename]}
These :bundle-cmd
configurations are merged with any :bundle-cmd
configurations in your .cljs.edn
file so that you can override
either the default ones.
:auto-bundle
also adds an important config to :closure-defines
in
your compiler options when not using :optimizations
:none
.
It adds:
{:closure-defines {...
cljs.core/*global "window"}}
:output-to
Don't specify the :output-to
compiler option. Figwheel will take
care of this for you and normally it's just an intermediate file. You are
probably much more interested in the output of the bundler.
If you must specify where the output of the compiler is sent use
:output-dir
and Figwheel will specify an [:output-dir]/main.js
file for you.
It's important to remember that all the output-files still have to be accessible from the browser (i.e. served by the web-server) when developing, as these files are not included in the output bundle.
:final-output-to
:final-output-to
is a Figwheel option, NOT a ClojureScript compiler option. Place it in the metadata section of your build config.
If you don't want to use the default location and need to specify a
specific location for your build's final bundled asset its probably
best to supply a :final-output-to
config option.
You don't need to specify :final-output-to
if you are not using the
built in Figwheel REPL host page, Extra-Mains, or
Auto-testing.
However, its helpful for Figwheel to know where the final bundled
asset of your build is located. If the location is known then Figwheel
can provide you a REPL without having to create and index.html
page.
Figwheel can also munge the name of the :final-output-to
to create
bundles for Extra-Mains and
Auto-testing.
:bundle-cmd
The :bundle-cmd
compiler option is not required. It specifies what
to do with the a compiled ClojureScript output file.
You can skip using :bundle-cmd
entirely and this would require that
you run the bundler manually AFTER your files have been initially
compiled.
This can be unpleasant because it's rather nice to run a Figwheel build command and have the browser window pop open along with a running REPL.
When you omit the :bundle-cmd
you will need to launch figwheel
first. The browser will pop open and display your custom index.html
application page but it will be broken because the bundled JavaScript
isn't available yet. Next, you will need to run your bundler and
reload the browser. At this point, everything should be up and running
fine. However, this is not the best experience and why using the
:bundle-cmd
is helpful.
Another reason the :bundle-cmd
is helpful is to provide Figwheel a
template of a command that can create bundles for your build. If you
create a :bundle-cmd
with the keywords :output-to
and
:final-output-to
, Figwheel will be able to reuse that command with
different parameters to create slightly different bundles for things
like Extra-Mains and
Auto-testing.
When filling in the :bundle-cmd
template Figwheel also replaces the
:final-output-dir
and :final-output-filename
template keywords
(this helps with commands that require them). It obtains these values
from the :final-output-to
value.
Figwheel by default only runs the :bundle-cmd
after the first
compile, this avoids the incurring the latency of bundling on every
single file change. This can significantly slow down hot-reloading
depending on your set up. You can change this with the :bundle-freq
Figwheel option.
:bundle-freq
The :bundle-freq
Figwheel option controls how often a the
:bundle-cmd
is called.
It has three settings :once
, :always
, and :smart
:
:once
- bundles only once after the initial build compile. This
helpful if you want to launch a webpack
watch command on the
command line after you start your build.:always
- bundles after every ClojureScript compile. This is fine
when you are starting out and bundle times are short. This however
increases the total compile time and can slow down your hot-reload
time.:smart
- re-bundles only when your :output-to
or npm_deps.js
changes thus trying to only bundle when you have included a new node
dependency in your CLJS code. This will cover most cases where you
need to rebundle but is no where near a complete as launching a
webpack
watcher.The default is :once
which is the most conservative setting.
I think that using :smart
is probably the best choice if you are not
using a webpack
watcher.
Keep in mind that a hot-reload will NOT pick up changes from a re-bundling. You have to refresh the browser to get those changes. So if you add a node dependency and it gets re-bundled into your JS bundle you won't see it until you reload the browser.
webpack.config.js
It's important to note that you can still use a webpack.config.js
to
specify various configurations for your bundle. The CLI options simply
override the configuration in the webpack.config.js
file. If an
input file is specified on the command line it will simply override
any entry
supplied in the Webpack config. If an -o
is supplied it will
any output
supplied in the Webpack config as well.
This frees you to configure your Webpack bundle as you need.
The motivation is that we want to have a JavaScript bundler process our ClojureScript output so that it can bring in and bundle all of our NPM dependencies. So we have two output files the output file from ClojureScript and the final bundled output from a JavaScript bundler like Webpack or Parcel.
When you use the :bundle
target, the ClojureScript compiler does
just this. We're going to examine how this is accomplished.
Let's look at an example default output file when you don't supply a
:target
option and thus get the default browser target output.
window.CLOSURE_UNCOMPILED_DEFINES = {"figwheel.repl.connect_url":"ws:\/\/localhost:9500\/figwheel-connect?fwprocess=29ee5f&fwbuild=devy"};
window.CLOSURE_NO_DEPS = true;
if(typeof goog == "undefined") document.write('<script src="/cljs-out/devy/goog/base.js"></script>');
document.write('<script src="/cljs-out/devy/goog/deps.js"></script>');
document.write('<script src="/cljs-out/devy/cljs_deps.js"></script>');
document.write('<script>if (typeof goog == "undefined") console.warn("ClojureScript could not load :main, did you forget to specify :asset-path?");</script>');
document.write('<script>goog.require("figwheel.core");</script>');
document.write('<script>goog.require("figwheel.main");</script>');
document.write('<script>goog.require("figwheel.repl.preload");</script>');
document.write('<script>goog.require("devtools.preload");</script>');
document.write('<script>goog.require("figwheel.main.system_exit");</script>');
document.write('<script>goog.require("process.env");</script>');
document.write('<script>goog.require("hello_world.core");</script>');
In the code above you can see the standard ClojureScript bootstrap code that loads your application into the browser (along with the figwheel preloads).
When we are working with NPM, we want the ClojureScript compiler to
resolve all of our references to NPM libraries in our ns
declarations and then add them into this file somehow so we can call
webpack
on it to import them.
For example when we have a namespace like above:
(ns hello-world.core
(:require [moment]))
We want ClojureScript to make a note of that and then emit something
that can bundled and that will process and include the moment
NPM
library.
With this in mind let's look at the main output-to file when we use
the :bundle
target.
import {npmDeps} from "./npm_deps.js";
window.CLOSURE_UNCOMPILED_DEFINES = {"figwheel.repl.connect_url":"ws:\/\/localhost:9500\/figwheel-connect?fwprocess=87057f&fwbuild=dev","cljs.core._STAR_target_STAR_":"bundle"};
window.CLOSURE_NO_DEPS = true;
if(typeof goog == "undefined") document.write('<script src="/cljs-out/dev/goog/base.js"></script>');
document.write('<script src="/cljs-out/dev/goog/deps.js"></script>');
document.write('<script src="/cljs-out/dev/cljs_deps.js"></script>');
document.write('<script>if (typeof goog == "undefined") console.warn("ClojureScript could not load :main, did you forget to specify :asset-path?");</script>');
document.write('<script>goog.require("figwheel.core");</script>');
document.write('<script>goog.require("figwheel.main");</script>');
document.write('<script>goog.require("figwheel.repl.preload");</script>');
document.write('<script>goog.require("devtools.preload");</script>');
document.write('<script>goog.require("figwheel.main.system_exit");</script>');
document.write('<script>goog.require("process.env");</script>');
document.write('<script>goog.require("hello_world.core");</script>');
window.require = function(lib) {
return npmDeps[lib];
}
The first line is new. Let's look at it:
import {npmDeps} from "./npm_deps.js";
OK so we are importing an npm_deps.js
file. Let's look at its contents as well:
module.exports = {
npmDeps: {
"moment": require('moment') }
};
So, the npm_deps.js
file is a JavaScript bundler ready file that was
generated by the CLJS compiler. The compiler looked at all of our ns
declarations and resolved the NPM libraries by looking at the
node_modules
directory.
Now when we run a bundler on our main.js
output file the bundler will
resolve the npm_deps.js
file and then will resolve and include all
NPM libs we are using.
It's important to note that the main.js
output file is importing the
./npm_deps.js
file relatively so both these files need to be in
the same directory. ClojureScript will let you break this easily if
you supply an :output-to
option that isn't in the output directory.
Now let's look at the last lines of our main.js
output file.
window.require = function(lib) {
return npmDeps[lib];
}
These lines shim require
so that it requiring a NPM library in
ClojureScript works correctly.
Further is we look at the output of compiling the
hello_world/core.cljs
file you will see this:
// Compiled by ClojureScript 1.10.773 {:target :nodejs}
goog.provide('hello_world.core');
goog.require('cljs.core');
hello_world.core.node$module$moment = require('moment');
console.log(hello_world.core.node$module$moment);
cljs.core.println.call(null,["Hello there it's ",cljs.core.str.cljs$core$IFn$_invoke$arity$1(hello_world.core.node$module$moment.call(null).format("dddd"))].join(''));
//# sourceMappingURL=core.js.map
The interesting line is where a scoped local reference of the moment
is created.
hello_world.core.node$module$moment = require('moment');
The :bundle
target uses the current functionality of the :nodejs
target which emits these requires. And while this works fine for
Node
we need to shim require
to support it in the browser.
Keep in mind this is all only true when we are developing in
:optimizations :none
. When we want to create a single compiled
artifact to deploy for production, using :simple
or :advanced
optimization mode ClojureScript will put together a single output file
with the requires in it. Then the bundler will resolve and replace
these top require
s as it normally does.
The most likely thing that will happen is you will have a bad
:bundle-cmd
.
:bundle-cmd
option, run the figwheel build and then run the
webpack command from your terminal/shell environment to see what
errors are showing up:output-to
file and npm_deps.js
The :output-to
file emitted by the :bundle
target imports the
npm_deps.js
file. This can only work when the :output-to
file is
in the same directory as the npm_deps.js
file. I.E. the :output-to
file has to be in the :output-dir
for the build.
{::comment} TODO
Can you improve this documentation? These fine people already did:
Bruce Hauman, bpringe, Michael Camilleri, Alan Thompson, Jindrich Mynarz & Jonathon McKitrickEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close