If you just want to see a working example, clone this repo.
My goal is to help you setup a working dev environment without overwhelming you with details, as quickly as possible.
This guide is, hopefully, many hours of Googling, trial and error saved and compiled into 3 articles.
I will try my best to briefly explain the role of each tool and give a rationale for my choice so you can decide whether it’s the right one for you or not.
If you have suggestions, corrections or enhancements, please tell me in the comments and I’ll update this post.
Also if you don’t like having many dependencies… I’m sorry.
- The first part will cover the bundler (Rollup), compiler (Babel), type checker (Flow) and HTTP server (serve).
- The second part will setup a simple Service Worker capable of precaching our application.
- The third and last part will focus on testing (jest and cypress), continuous integration and deployment.
At the bottom of each post, you will find a link to its own branch in the example repo (without the stuff from the next articles).
- We will need Node.js current or, at least, lts, as well as an up-to-date package manager such as yarn or npm. In my examples, I will use npm.
- Our app directory should be created with a package.json file (
npm init), directories public/, public/js/ and src/ .
- Commands should be run from the root of the app directory unless specified otherwise.
- Web server root will be public/
- Rollup will output our builds in public/js/
- source code will go in src/
Babel will convert our ES201* + JSX + Flow codebase into code comprehensible by our targets: the browsers and platforms we want to support.
It uses plugins and presets to do its job, and can be configured in a .babelrc file in your app directory.
npm install --save-dev @babel/cli @babel/core @babel/plugin-proposal-class-properties @babel/plugin-syntax-dynamic-import @babel/plugin-transform-runtime @babel/preset-env @babel/preset-flow @babel/preset-react
This will install everything we need to get babel to support React+JSX, Flow and dynamic import.
There are multiple ways to configure babel, but I prefer .babelrc : our configuration can be used automatically by any tool that needs it (rollup, jest and eslint in our case).
- preset-env is configured to target browsers with at least .25% global usage, except opera mini,
- we specifically need dynamic import (for code splitting), object rest spread and class properties (commonly used with React class components).
Rollup is a module bundler which will transform, tree-shake and regroup our ES modules into optimized chunks to be fed to (in our case) browsers.
(Tree-shaking is an imaginative name for the process of removing unnecessary code from our bundle.)
Why Rollup ? It’s extensible, supports code-spitting, outputs ES modules and other formats such as CommonJS, which gives us options to target older browsers with little compromise.
npm install --save-dev rollup rollup-plugin-babel rollup-plugin-commonjs rollup-plugin-clear rollup-plugin-node-builtins rollup-plugin-node-globals rollup-plugin-node-resolve rollup-plugin-replace rollup-plugin-terser
Rollup needs plugins to understand our code: how to import external modules, how to run Babel, how to minify code, insert constants, etc.
Since uglify-js isn’t under active development, Terser is our minifier of choice.
Rollup reads its configuration from a rollup.config.js file in our app directory. It can return a function that takes CLI arguments and returns an object, like so (explanation below):
In order to control when to minify our code and when to compile in production mode, we use 2 arguments, “prod” and “mini”. That way, we can call rollup with or without these arguments :
rollup -c --prod --mini # will build for production
rollup -c # will build for development
I won’t further detail each individual plugin, but the general idea is : resolve external dependencies, handle CJS modules, run babel, minify if needed, build in 2 formats, write to disk.
namedExports property in the commonJS plugin. When we
import (with the ES2015 syntax) named exports from CJS modules, Rollup does its best to find the right value, but sometimes fails to do so (in my experience anyway). In that case, you need to explicitly define exports here.
src/index.jsx will be our “entrypoint”. Some might call it “main.jsx” or “app.jsx” but you get the point: this is where execution should start.
You can have multiple entrypoints (read more in the Rollup docs).
When Rollup sees a dynamic
import("...") statement in your code, it will automatically create a separate “chunk” in your bundle.
Additionally, React provides an API for lazily loading components, you can read about it here.
There are many others plugins (for livereload, etc.). If you want to try some, start here.
Loading our bundles from index.html
Right now, ES dynamic import support is far from the norm. It is supported on Chrome, but still behind a flag in Firefox 66, and will never make it to IE11.
This means we need to have a fallback: thus, SystemJS.
I wrote an explanation on how to use both formats accordingly here (that’s the solution I use in the example repo).
Alternatively, there is now an abstraction for this directly in Rollup, see dynamicImportFunction.
Flow is a static type checker. If you haven’t used one before, the main idea is that a type checker doesn’t seem very time efficient to present you, but it’s a life saver to future you (meaning: when refactoring).
npm install --save-dev flow-bin
In order to get type definitions for external libraries you may be using, you can install globally flow-typed and run the following command:
npm install -g flow-typed
flow-typed install # will create a flow-typed/ directory
This is the base configuration I use (.flowconfig):
To enable Flow in a file, simply add
//@flow at the beginning.
Depending on your editor/linter/tooling, you may want to also install flow-bin globally.
Serve will be our http server. Again, there are many alternatives, but we need something that can provide some configuration over headers and redirects (in the case of an SPA).
npm install --save-dev serve
The configuration is simple, for now. We will set headers later, when we deal with Service Workers (serve.json)
We can use npm scripts to alias some of our most useful commands. This will be helpful later when we setup CI/CD.
Add this in the “scripts” section of package.json:
npm run build # production build
npm run watch # dev loop
npm run serve # then browse to http://localhost:5000
In part 2, we will precache our app with Service Workers.