This is a deprecated version of this document. See the latest version here.
This guide is modeled after the
ClojureScript Webpack Guide. If
you prefer a more concise guide, feel free to head over there now. This
guide will also demonstrate how to use the
:npm config option.
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 natural friction because ClojureScript embraced the Google Closure Compiler and its method of declaring libraries, which is quite different than NPM's.
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.
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 assume you are starting from the
base hello-world.core example which is used
throughout this documentation.
It is assumed that you have installed NodeJS along with npm in your
development environment.
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.
index.js file to export the needed libraries to the global context.We will need to initialize npm for our hello-world project.
Make sure you are in the root directory of the project and execute:
$ npm init -y
Let's say we want to use the react and react-dom libraries in our
application.
We'll use npm to add them in the usual manner:
$ npm add react react-dom
This should download and install the needed libraries. Now there will
be a package.json file and a node_modules directory in your
project.
First we will install webpack and webpack-cli:
$ npm add webpack webpack-cli
Next we will create a basic Webpack configuration file for our
bundle. In webpack.config.js place the following code:
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'index.bundle.js'
}
}
If you aren't familiar with Webpack the above config file is stating
that when we run webpack it will bundle all the resources needed in
src/js/index.js into a single dist/index.bundle.js file.
index.js fileThe src/js/index.js file is where we will require the libraries that
we need and export them to the global context so that we can access
them from ClojureScript.
Continuing with the example, let's export the react and react-dom
NPM libraries to the global window context of the browser.
Place the following code in the src/js/index.js file:
import React from 'react';
import ReactDOM from 'react-dom';
window.React = React;
window.ReactDOM = ReactDOM;
The above code imports the NPM libraries we need and makes them
available in the global context on window.
Keep in mind that the
importstatements are dependent on how the individual JavaScript library exports its functionality. You will need to refer to the documentation for the given library to understand how to import it properly.
We can now use Webpack to bundle all the NPM libraries needed for the
above index.js file into a single file. We will use the npx
command to run Webpack:
npx webpack
If all goes well you will see that a dist/index.bundle.js file was
created.
Important: When you are managing your own NPM dependencies, as we are here, and you have a
node_modulesdirectory in the root of your project directory, you will need to set:npm-depstofalsein your build config file (dev.cljs.ednin this example). Otherwise, the ClojureScript compiler will scan it and make other decisions based on its presence, and this can lead to confusing errors.
Let's do this now and change the dev.cljs.edn file and set
:npm-deps to false:
{:main hello-world.core
:npm-deps false}
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.
Now we need to let the ClojureScript compiler know that we are using
some globally exported libraries in dist/index.bundle.js.
In the dev.cljs.edn file we'll add the following :foreign-libs
entry:
{:main hello-world.core
:npm-deps false
:infer-externs true
:foreign-libs [{:file "dist/index.bundle.js"
:provides ["react" "react-dom"]
:global-exports {react React
react-dom ReactDOM}}]}
Understanding the above configuration is important, so I'm going to explain each part.
The
:foreign-libs compiler option
helps you map foreign libraries to libraries that you can require
and use from inside your ClojureScript source code. Foreign libraries
are dependencies that don't follow the Google Closure way of defining
a library.
Since we want to be able to require and use the react NPM library from
our ClojureScript code, we have to provide the compiler and the
Closure bootstrapping/require process with the location
where our custom file falls in the dependency tree.
So :foreign-libs is a list of data structures that provide this
information for individual JavaScript files.
Let's look at our entry in :foreign-libs and see how this plays out.
{:file "dist/index.bundle.js"
:provides ["react" "react-dom"]
:global-exports {react React
react-dom ReactDOM}}
Well it's pretty obvious that we need a :file key as we are adding
meta information to a specific JavaScript file. The ClojureScript
compiler does not need for this file to be on the classpath, and it will
copy the file into the :output-dir at dist/index.bundle.js.
Next let's look at the :provides key. The :provides key tells the
compiler that this file provides the listed libraries. In this case
:provides key tells the compiler that when someone requires react
or react-dom then the file dist/index.bundle.js must be supplied.
The names that you list in the :provides key are the same names that
you will use in your :require expressions in your namespace
declarations.
(ns hello-world.core
(:require [react]
[react-dom]))
Actually, using the :file and :provides keys is enough to start
using ReactJS from our source code, but we will only be able to
reference it via JavaScript, not via the react namespace.
For example this works:
(ns hello-world.core
(:require [react]
[react-dom]))
(js/console.log js/React)
This works because the compiler now knows to include the
dist/index.bundle.js file when react is required, and this bundle
file exports React to window.React and thus we can reference it. But
the following will not work:
(ns hello-world.core
(:require [react]
[react-dom]))
(js/console.log react)
Referring to react directly in the source code won't work if we only
use the :provides key. This won't work because there is no way for
the compiler to know what should be bound to react.
This is where the :global-exports key comes in. It maps the name you
specify in the require statement to a specific global resource in your
JavaScript environment.
Looking at the :global-exports key and our src/js/index.js file
together, let's see how they map to one another.
:global-exports {react React
react-dom ReactDOM}
import React from 'react';
import ReactDOM from 'react-dom';
window.React = React;
window.ReactDOM = ReactDOM;
The React value in the global exports map refers specifically to the
presence of window.React in the index.js file.
To further understand what is happening here let's look at how this example
compiles when we have specified :global-exports.
This src/hello_world/core.cljs file:
(ns hello-world.core
(:require [react]
[react-dom]))
(js/console.log react)
compiles to the following JavaScript when using :optimizations level
:none:
goog.provide('hello_world.core');
goog.require('cljs.core');
goog.require('react');
goog.require('react_dom');
hello_world.core.global$module$react = goog.global["React"]; // <--
hello_world.core.global$module$react_dom = goog.global["ReactDOM"]; // <--
console.log(hello_world.core.global$module$react);
Looking at the above compiled JavaScript you can see the two lines
that grab React and ReactDOM from the global context
(goog.global is window in this case) and binds them to a "local"
name. You can then see how the local
hello_world.core.global$module$react is used in the console.log
statement.
If you don't use :global-exports and only use :provides this name
binding doesn't happen and thus you are required to refer to your
libraries via the js/ prefix.
Now you should have a good idea of how to specify :foreign-libs
entries to make NPM libraries available for ClojureScript consumption.
Next let's look at how Figwheel can automate the generation of these
:foreign-libs entries for you.
You can learn more about importing JavaScript via
:foreign-libshere
:foreign-libs entries for NPMWhen you look at the shape of a :foreign-libs entry and the format
of our example src/js/index.js file you may wonder if we can just
automate this.
If you want, Figwheel will try to read your index.js file and
generate a :foreign-libs entry for you. This is intended to help cut
out some of the pain of consuming NPM libraries.
The :npm Figwheel option will do this for
you. Here is an example of the configuration in our dev.cljs.edn
before and after using the :npm key.
Before:
{:main hello-world.core
:npm-deps false
:infer-externs true
:foreign-libs [{:file "dist/index.bundle.js"
:provides ["react" "react-dom"]
:global-exports {react React
react-dom ReactDOM}}]}
After:
^{:npm {:bundles {"dist/index.bundle.js" "src/js/index.js"}}}
{:main hello-world.core}
This may not seem like much of a reduction, but consider the
fact that once you define your bundle in the :npm key you no longer
have to change the dev.cljs.edn file when you add a new NPM library
in your index.js file.
You will still need to re-run webpack when you change the index.js
file but you won't have to add an entry to :global-exports for each
new library.
When you supply an :npm > :bundles configuration Figwheel will add
both :infer-externs true and :npm-deps false to your compile
options as well, but only if they are not already defined.
How :npm > :bundles reads your index.js file to create a :foreign-libs entry
If you supply an index.js file with lines in it that start with
window. as is the case in the example above with the lines that
start with window.React and window.ReactDOM, then Figwheel will take
the React and ReactDOM names and
kebab-case them into react and
react-dom. It will then use these identifiers in the
:global-exports like so:
:global-exports {react React
react-dom ReactDOM}
It will then take the keys of the :global-exports map and turn them
into a :provides entry like this:
:provides ["react" "react-dom"]
:global-exports {react React
react-dom ReactDOM}
Then Figwheel will get the :file from the name of the Webpack bundle
in the :npm > :bundles declaration to create the complete
:foreign-libs entry before sending it to the compiler.
:foreign-libs [{:file "dist/index.bundle.js"
:provides ["react" "react-dom"]
:global-exports {react React
react-dom ReactDOM}}]
Sometimes the kebab-case can fail to generate the library name that
you need. In this case you can precisely control the library name by using
the window["react"] = React; form to assign the library to the global
context instead. For instance you could use this to override some code
that is relying on cljsjs.react via window["cljsjs.react"] = React; and now when something requires cljsjs.react they will get
your NPM version of ReactJS.
The :bundles functionality can also read goog.global.ReactDOM = ReactDOM; and goog.global["cljs.react-dom"] = ReactDOM; in your
index.js file.
If you prefer to use the goog.global form you should protect against
it not being there under advanced compile, like this:
;; ensure that goog global exists under advanced compile
var goog = goog || {global: window};
import React from 'react';
import ReactDOM from 'react-dom';
goog.global.React = React;
goog.global.ReactDOM = ReactDOM;
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |