diff --git a/docs/docs/npm.md b/docs/docs/npm.md index 69483e0..b32244b 100644 --- a/docs/docs/npm.md +++ b/docs/docs/npm.md @@ -5,6 +5,8 @@ category: docs order: 12 --- + + # Using NPM
NPM is the @@ -13,18 +15,17 @@ tremendous amount of valuable functionality. This guide will show you how to include and consume NPM packages in your ClojureScript codebase.
-This guide is modeled after the -[ClojureScript Webpack Guide](https://clojurescript.org/guides/webpack). If -you prefer a more concise guide, feel free to head over there now. - > Npm usage in Figwheel has changed significantly. For reference purposes the > original version of this document can be found > [here](/docs/npm_archived) > These instructions require `org.clojure/clojurescript` >= `1.10.773` +> and `com.bhauman/figwheel-main` >= `0.2.6`.
-**Quick Instructions** +**Quick Reference** + +*Read the rest of this document and come back here for a quick reference.* Set the [`:target` compiler option](https://clojurescript.org/reference/compiler-options#target) @@ -41,11 +42,12 @@ to to ensure the output file is bundled after a compile. Figwheel will fill in `:output-to` and `:final-output-to`. -> You will want to make sure that the `:output-to` file is in the -> `:output-dir` directory so the bundler can find the assets it needs. - 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** * [:bundle-once](/config-options#bundle-once) @@ -82,7 +84,41 @@ 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. -## Importing NPM libraries into your project +## The Overview + +We are going to utilize a JavaScript bundler like [Webpack][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: + +1. compililation by the ClojureScript compiler to an intermediate file +2. bundled together with its NPM dependencies to a final output + file that we will load into the browser + +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. + +## Getting started with NPM libraries into your project We are going to assume you are starting from the [base example][base-example-gist]. @@ -165,7 +201,17 @@ option](https://clojurescript.org/reference/compiler-options#target) 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](https://clojurescript.org/reference/compiler-options#bundle-cmd) +The [`:bundle-cmd` compiler +option](https://clojurescript.org/reference/compiler-options#bundle-cmd) +is set to + +```clojure +{: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 @@ -234,6 +280,247 @@ Well we successfully used an [NPM][npm] package from our ClojureScript code. Now you can `npm add` other JavaScript NPM packages and use them from ClojureScript +## Configuration Tips + +### Don't specify `: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. + +### Using `: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](/docs/extra_mains.html), or +[Auto-testing](/docs/testing.html#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](/docs/extra_mains.html) and +[Auto-testing](/docs/testing.html#auto-testing). + +### Using `:bundle-cmd` + +The `:bundle-cmd` compiler option is not required. It specifies what +to do with the a compiled ClojreScript 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](/docs/extra_mains.html) and +[Auto-testing](/docs/testing.html#auto-testing). + +> Using a `:bundle-cmd` that specifies an `--output` does not preclude +> you from having a `webpack.config.js` as the `--output` or `-o` flag +> will override the output file settings in the config. + +Figwheel by default only runs the `:bundle-cmd` after the first +compile, this avoids the incuring the latency of bundling on every +single file change. This can significantly slow down hot-reloading +depending on your set up. + +If you want bundling to happen after every compile set the +`:bundle-once` Figwheel option to `false` + + +## How NPM support works + +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][webpack] or [Parcel][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. + +```javascript +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(''); +document.write(''); +document.write(''); +document.write(''); +document.write(''); +document.write(''); +document.write(''); +document.write(''); +document.write(''); +document.write(''); +document.write(''); +``` + +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: + +```clojure +(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. + +```javascript +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(''); +document.write(''); +document.write(''); +document.write(''); +document.write(''); +document.write(''); +document.write(''); +document.write(''); +document.write(''); +document.write(''); +document.write(''); +window.require = function(lib) { + return npmDeps[lib]; +} +``` + +The first line is new. Let's look at it: + +```javascript +import {npmDeps} from "./npm_deps.js"; +``` + +OK so we are importing an `npm_deps.js` file. Let's look at its contents as well: + +```javascript +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. + +```javascript +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: + +```javascript +// 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. + +```javascript +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. + +## Troubleshooting + +### Bad bundle command + +The most likely thing that will happen is you will have a bad +`:bundle-cmd`. + +* Please check that the command that is logged is the +command that you expect. +* comment out the `: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 + +### The relationship between the `: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. + +[webpack]: https://webpack.js.org/ +[parcel]: https://parceljs.org/ [base-example-gist]: https://gist.github.com/bhauman/a5251390d1b8db09f43c385fb505727d [npm]: https://www.npmjs.com/