Building web applications with Rollup + Babel + React
As some of my ReactJs projects have gotten larger I needed to start dividing up my code into multiple files and package them up with some kind of javascript “bundling” tool. I’ve settled at least for now on Rollup.
And as is standard with React, if you want to use JSX, you need a JS transpiler – Babel.
Once you settle on using these two tools you get a lot of benefits. Projects can we written in a modular way. It integrates easily with NPM, so its very simple to add in open source JS modules. You can use the latest javascript features and compile your JS to be compatible with most browsers.
Babel, and especially Rollup are relatively new tools and are still changing frequently. Documentation on how to set up the environment is hard to piece together since there are few complete guides.
I’ll go through the environment I put together. Most of these tools are really new to me – so I’ll go through a lot of stuff that is pretty basic.
Setting up NPM
NPM is the “node package manager”. Its a system for installing javascript modules. NPM installs as part of NodeJs. So you should start by installing it. On apt-get Linux systems like Debian its:
sudo apt-get install -y npm
Coming from a c/c++ background one of the things that bothered me at first is the way NPM installs things. In c you install libraries by default in directories that are available system wide. The compiler is configured to find your libraries in these directories.
With NPM most of the libraries you use – modules in JS – are installed locally as part of your project.
To install a module with npm:
npm i {module name}
With NPM the default way it installs things is to put them in a node_modules
sub-directory of the directory where you run it. The modules you install are meant to be used by the current project only.
So for example if you create a project myProject
the environment might look like this:
~/myProject // <-- your source files can go here ~/myProject/src // <-- or even here ~/myProject/node_modules // <-- npm installed modules go here
You can also use NPM to install modules globally:
npm i -g {module name}
They will go into /usr/local/bin
. I was tempted to install everything global – but this does not work well. As a rule you should install tools that you run globally, and modules that you will use locally.
That is, install babel and rollup globally. Install modules that you will build into your projects, or plugins for babel and rollup locally.
In the root of your project directory create a file package.json
. This will contain the npm configuration information for your project. Your file might look something like this:
{ "author": "Rafael", "name": "myProject", "version": "0.0.1", "devDependencies": {}, "dependencies": {} }
Enter your name for author, and give your current project some kind of name and version number. Every time you install a module for your project you can add a line to this file for that module. devDependencies
is for modules that are used during development. dependencies
is for modules that you need when your project is running.
For web applications that run in the browser and are packaged up in a single file, all dependencies go into devDependencies
.
You can automatically add lines to dependencies as you install modules with some command line flags:
npm -i -D {module} // update devDependencies npm -i -S {module} // update dependencies
These flags are the same as the --save
and --save-dev
flags. For your web projects always install using -D
. Once you are done, its easier to go back into package.json
and remove lines for things you don’t need than to try to figure out the lines you need because you didn’t use -D
.
You should check package.json
into source control with the rest of your code – but not the contents of node_modules
. If you’ve set up the package files correctly, you can always re-create the modules directory by just running:
npm i
To see what you have installed locally run:
npm ls -p --depth=0
To see what is installed globally:
npm ls -pG --depth=0
Installing Babel and Rollup
Install them like this:
npm i -g babel-cli rollup
Now install all the modules we need locally. You the easiest way is to start with a package.json
file that lists them:
{ "author": "My Name", "name": "myProject", "version": "0.0.0", "devDependencies": { "babel-cli": "^6.18.0", "babel-plugin-external-helpers": "^6.18.0", "babel-plugin-syntax-decorators": "^6.13.0", "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-preset-es2015-rollup": "^3.0.0", "babel-preset-react": "^6.16.0", "babel-preset-stage-0": "^6.16.0", "react": "^15.0.2", "react-dom": "^15.0.2", "rollup-plugin-babel": "^2.7.1", "rollup-plugin-commonjs": "^5.0.5", "rollup-plugin-node-resolve": "^2.0.0", "rollup-plugin-replace": "^1.1.1", "rollup-plugin-uglify": "^1.0.1" }, "dependencies": {} }
This is a good minimal list of packages that you will want for this build environment. They are not all technically required – but practically you will need each of these to make a reasonable working environment.
Now install everything by running:
npm i
Configuring Babel
We will configure Rollup to use Babel as one of its build stages when bundling. This means that the Babel configuration will be pretty simple. Configure .babelrc
like this:
{ "presets": [ "es2015-rollup", "react" ], "plugins": [ "external-helpers", "transform-decorators-legacy" ] }
Presets are sets of babel configurations designed for a particular use case. We want just the ones for rollup and react.
The plugins add a few pieces of additional functionality that while not strictly necessary, are things you’re going to want by default. Babel adds some boilerplate code throughout your code.
The external-helpers
plugin will create a few functions at the top of your script and avoid inlining them multiple times throughout your code.
The transform-decorators-legacy
plugin allows Babel to handle ES6 decorators like the @
in:
@ReactRedux.connect( ) class StatusContent extends React.Component
The RFC for the decorators is currently in transition. There is a transform-decorators
plugin that matches the current RFC. But this is likely to change, and in the mean time people are using the older implementation as it existed in Babel 5. So until the new decorators are settled – we use the “legacy” decorator syntax.
Configuring Rollup
Configure rollup.config.js
:
import babel from 'rollup-plugin-babel'; import commonjs from 'rollup-plugin-commonjs' import nodeResolve from 'rollup-plugin-node-resolve' import uglify from 'rollup-plugin-uglify' import replace from 'rollup-plugin-replace' export default { entry: 'main.js', dest: 'myProject.js', format: 'iife', plugins: [ babel({ exclude: 'node_modules/**' }), nodeResolve({ jsnext: true }), commonjs({ include: 'node_modules/**', namedExports: { './node_modules/react/react.js': [ 'cloneElement', 'createElement', 'PropTypes', 'Children', 'Component' ], } }), replace({ 'process.env.NODE_ENV': JSON.stringify( 'production' ) }), uglify({ compress: { screw_ie8: true, warnings: false }, output: { comments: false }, sourceMap: false }) ] }
This configuration is the product of a lot of reading and a lot of trial and error. We can go through what each line means and why you want it, in this annotated version of the config file:
// this is the rollup plugin that adds babel as a compilation stage. import babel from 'rollup-plugin-babel'; // when importing packages, rollup does its best to figure out // what is being exported from modules designed for commonjs. This process // is imperfect and there are times that you need to manually specify what // symbols should be imported. import commonjs from 'rollup-plugin-commonjs' // this is needed to allow rollup to find modules in the node_modules directory. import nodeResolve from 'rollup-plugin-node-resolve' // this is a minification stage. It does some code rewriting. There are // alternatives, like using closure. But this one seems to work best. import uglify from 'rollup-plugin-uglify' // This is a simple utility plugin that allows you to make changes in the // output code. Sometimes after all bundling is complete, you need to make some // final patches to make the code work. import replace from 'rollup-plugin-replace' export default { // this is the entry point for your script. All the other code that // gets included will come from import statements. entry: 'main.js', // this is the output file. dest: 'myProject.js', // this is the output format. iife is best for web apps meant to run in a // browser. iife means that the script is packaged as a self contained self // executing function. format: 'iife', // this section configures each of the plugins imported above plugins: [ // tell babel not to compile stuff out of node_modules. I think this // makes the compilation step run faster. babel({ exclude: 'node_modules/**' }), nodeResolve({ jsnext: true }), commonjs({ // where to search for modules when you import them. if the // module path is not given explicitly, rollup will search // for them here. include: 'node_modules/**', // this is where you patch modules that don't export their symbols // cleanly. namedExports: { // react appears to be one of those. Either that, or I'm not // importing it correctly in my code. Regardless this is an // example of telling rollup to extract the following symbols // from a package as if they were exported. './node_modules/react/react.js': [ 'cloneElement', 'createElement', 'PropTypes', 'Children', 'Component' ], } }), // If you don't patch this the "process" symbol required by react will // not be defined. All you need to do here is set that string to either // 'development' or 'production' depending on which kind of build you // are making. replace({ 'process.env.NODE_ENV': JSON.stringify( 'production' ) }), // configuration for the uglify minifier. uglify({ compress: { screw_ie8: true, warnings: false }, output: { comments: false }, sourceMap: false }) ] }
The configuration above is a good default for building a final production version of your code. During development you will want to configure react for development, and not run the minifier. There may be a better way of switching between development and production configurations, but for now I’m doing it like this:
import babel from 'rollup-plugin-babel'; import commonjs from 'rollup-plugin-commonjs' import nodeResolve from 'rollup-plugin-node-resolve' import uglify from 'rollup-plugin-uglify' import replace from 'rollup-plugin-replace' var productionConfig = { entry: 'status-react.jsx', dest: '../files/status-react.js', format: 'iife', plugins: [ babel({ exclude: 'node_modules/**' }), nodeResolve({ jsnext: true }), commonjs({ include: 'node_modules/**', namedExports: { './node_modules/react/react.js': [ 'cloneElement', 'createElement', 'PropTypes', 'Children', 'Component' ], } }), replace({ 'process.env.NODE_ENV': JSON.stringify( 'production' ) }), uglify({ compress: { screw_ie8: true, warnings: false }, output: { comments: false }, sourceMap: false }) ] } var developmentConfig = { entry: 'status-react.jsx', dest: '../files/status-react.js', format: 'iife', plugins: [ babel({ exclude: 'node_modules/**' }), nodeResolve({ jsnext: true }), commonjs({ include: 'node_modules/**', namedExports: { './node_modules/react/react.js': [ 'cloneElement', 'createElement', 'PropTypes', 'Children', 'Component' ], } }), replace({ 'process.env.NODE_ENV': JSON.stringify( 'development' ) }) ] } export default developmentConfig;
The last line can be edited to point to the desired configuration.
Importing Modules in JS Source files
It was quite tricky to figure out the best way to import each of the standard modules that you need for react. Here is what the imports at the top of my entry point js file looks like:
import React from 'react' import ReactDOM from 'react-dom' import * as Redux from 'redux' import * as ReactRedux from 'react-redux' import { Provider } from 'react-redux'
As you can see, this project is using Redux. To add Redux to your project you will need to add the following two modules to package.json:
"react-redux": "^5.0.1", "redux": "^3.6.0",
Once you have this all set up, it should be easy to break up your code into modules. Put functions and classes into separate files in their own directory structure ( not in node_modules
) Things that will be called by code in another file prefix them with export
:
// module.js export function myFunction() { ... } export class myClass extends React.Component { ... }
Then in the js file that uses this module:
import { myFunction, myClass } from './module.js' // call it like this: myFunction();
or import like this:
import * as myModule from './module.js' // call it like this: myModule.myFunction();
Building Your project with Rollup
Build it like this:
rollup -c
If you set it up right it should just work.
Resources
RollupJs | Main page for the RollupJs project |
Babel | Main page for the Babel JS transpiler project |
React | Main page for the React JS framework |
npm-install Install a package | The NPM command line documentation opened to the page about installing modules. |
External Helpers | Documentation for the babel external helpers plugin |
Exploring EcmaScript Decorators | A good introductory article about ES6 decorators |
Babel Legacy Decorator plugin | Documentation for the plugin. Explains the difference between regular and legacy versions. |
Rollup Plugin Replace | Documentation for the Rollup replace plugin |
Immediately-invoked function expression | Explains the iife js design pattern. |
UglifyJS | Documentation for the UglifyJS minifier |
React-Bootstrap | The bootstrap UI framework ported to React |
Redux | A simple JS library for managing application state. |
React Redux | React bindings for Redux |
Really useful, cheers mate. Wasted loads of time googling trying to get a similar config
working but got up and running in about 5 minutes following your guide. Saved me a massive headache!