Rollup-based dev environment for JavaScript (part 1)

Camille Hodoul
Mar 22 · 5 min read

This is a step-by-step guide to setting up a development environment for JavaScript applications, with support for ES modules, dynamic import, code-splitting, JSX and Service Workers.

If you just want to see a working example, clone this repo.

There are as many different ways to do this as there are JavaScript developers, so this is my humble take on it.
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.

Summary

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).

Prerequisite

  • 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.

Directory structure

  • Web server root will be public/
  • Rollup will output our builds in public/js/
  • source code will go in src/

Babel

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).

Brief explanation:

  • 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

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

Notice the 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.

Input

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).

Output

We use 2 output formats : ESM (ES modules) for browsers that support dynamic import, system for those that don’t.

Code splitting

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.

Plugins

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

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).

Alternatives ? Flow is very much a challenger in the JavaScript static type checker space, as Typescript keeps growing. Use the one you and your team like (although if you are writing a library, I would recommend Typescript, at least type definitions).

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

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)

Scripts

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

See how our app looks so far (branch for this post).

In part 2, we will precache our app with Service Workers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade