Bundling web applications with Parcel

Published: January 07, 2018  •  Updated: February 16, 2018  •  javascript

When we write web applications, use modules and import 3rd party libraries, maybe use SCSS and TypeScript and target older browser we need a tool that helps us build and bundle our application into a package that we then can deploy to a web server.

We already have bundlers like Browserify, rollup.js and webpack but often they need quite a lot of configuration before they do something. Recently a new contender entered the arena: Parcel. Advertised features are speed and almost no configuration.

To see Parcel in action we first create a simple JavaScript application and then use Parcel to bundle it together.

Create an empty directory and a package.json file with the command npm init -y. Next create a subdirectory src that will host all the source files. Then copy the example files into the src folder. You find the complete source code on GitHub:
https://github.com/ralscha/blog/tree/master/parcel/js/src

The application consists of four JavaScript, a CSS and the index.html file. It reads earthquake data from the usgs.gov website and displays them on the screen. For parsing the CSV data the application depends on the PapaParse library (npm install papaparse).

Here a schematic overview of the application:
overview

The application uses ES6 (ES2015) modules with import and export. Parcel also supports the CommonJS syntax. All the different parts are linked together beginning from the main entry point (index.html). main.css and main.js are referenced with a link and script tag in index.html. You see in a moment why this important.

Next we install Parcel. You can install it either globally with

npm install -g parcel-bundler

or locally as project dependency with

npm install parcel-bundler -D

I prefer this approach because a new team member only needs to clone the project, call npm install and everything is set up.

When you installed Parcel globally you can package the application with

parcel build src/index.html

When Parcel is installed locally you can start it with npx

npx parcel build src/index.html

npx is the npm package runner that is part of the npm package since version 5.2.0. See more information about npx here.

The first parameter of the build command is the main entry point to the application. Parcel builds, beginning from this entry point, a tree of dependencies. It can only bundle files that are linked directly or indirectly with the main entry point. The entry point does not need to be a html file. You can also specify a JavaScript file.

When you open the dist/index.html file you see that the paths to the CSS and JavaScript file contain a /dist/ prefix. This would not work when we deploy the dist folder as root directory of the application. What we want in this case are relative paths.

You can specify that with the --public-url parameter

npx parcel build src/index.html --public-url ./

Parcel builds the applications by default into the dist folder.
You can change that with the -d option: npx parcel build src/index.html -d build

Parcel bundles out of the box all the modules and 3rd party modules into JavaScript files, minifies the CSS, HTML and JavaScript code with cssnano, htmlnano and uglify-es.
Then it revisions the CSS and JavaScript files with a content hash in the filename. This way a web server can serve these file with a cache expiry date far in the future.
It adjusts the paths in the index.html file to reference the built CSS and JavaScript files. And Parcel did all this without any configuration.

Calling Parcel with npx gets a bit tedious over time so my prefered solution is adding a task to the package.json file.

  "scripts": {
    "build": "parcel build src/index.html --public-url ./"
  },

Now we can start the build with npm run build

I also add a clean step that deletes everything in the dist folder before Parcel starts bundling. This removes leftover artefacts from previous builds.

"prebuild": "shx rm -rf dist/*",

This task depends on the shx library (npm install shx -D) which is a wrapper around ShellJS that provides platform independent shell commands.

Our complete production build system looks like this

 "scripts": {
   "prebuild": "shx rm -rf dist/*",
   "build": "parcel build src/index.html --public-url ./"
 },

package.json

Tasks starting with pre and post are started automatically by npm before and after a specific task. So you only have to enter the command npm run build and npm runs the prebuild, build and postbuild tasks in that order.


Environment Variables

Often an application needs a way to know if it runs in the development or in the production environment. Parcel automatically sets the NODE_ENV variable to production when you do a production build.

In JavaScript, you can access this variable with process.env.NODE_ENV.

   if (process.env.NODE_ENV === 'production') {
       //running in production
   } else {
       //running in development
   }   

Since version 1.5.0 you can also use .env for setting environment specific variables. Parcel automatically loads, with the help of this package, variables from a .env file into process.env.

For example, you have two environments development and production and you want to set variables. You create a .env file that contains the values for the production and a .env.development file with the values for the development. You don't have to repeat values in .env.development that don't change from .env. The dotenv library automatically merges the two together for development.

APP_NAME=Test
APP_ENV=production
APP_KEY=1234

.env

APP_ENV=development
APP_KEY=test_key

.env.development

In the JavaScript code you can access these variables through process.env.

const dotEnvContent = `APP_NAME = ${process.env.APP_NAME}<br>APP_ENV = ${process.env.APP_ENV}<br>APP_KEY = ${process.env.APP_KEY}`;    
document.getElementById('dotEnv').innerHTML = dotEnvContent;

src/init.js

In this example APP_NAME contains the value Test in development and production.


Development

During development, it would be a bit annoying if we had to build the application each time we change a file. Fortunately Parcel supports a development mode where it starts a web server, installs a watch process that rebuilds the application when files change and automatically reloads the changes into the browser. To start this mode you use the command

parcel src/index.html

or

npx parcel src/index.html

Or adding it as a task to the package.json file.

"scripts": {
   "start": "parcel src/index.html",
     ..
}

package.json

and start it with npm run start or shorter npm start. Parcel starts a web server on localhost and port 1234.

If you already have a web server running in your development environment you can start Parcel with the watch command. This rebuilds the app when files change and does code replacement in the browser but does not start the web server.

parcel watch src/index.html

Babel

The example application uses features like async/await (ES2017), arrow functions, let/const, template strings (ES2015) and it uses the Fetch API. This code runs fine on most modern browsers but will fail on older browsers like IE11. IE11 only supports ES5 and has no Fetch API implementation.

To support older browsers we need a tool that rewrites the code into ES5 code. Tools you can use for that are for example Babel and the Google Closure Compiler.

Since version 1.6.x of Parcel, you no don't need to configure anything at all. Parcel automatically transpiles your code with babel-preset-env.

If you use an older Parcel version, you need to install the package manually.

npm install babel-preset-env -D

and create .babelrc in the root of the project. You can still create a .babelrc file with version 1.6.x if you need to overwrite default settings.

{
  "presets": ["env"]
}

Unfortunately this would not work because we use constructs like async/await that, after they are rewritten, depend on Babel helper methods. Babel only transforms the code it does not automatically insert polyfills or helper methods into the code. Therefore, we have to add them manually and for that we install the babel-polyfill package

npm install babel-polyfill

To solve the missing Fetch API implementation we add the whatwg polyfill.

npm install whatwg-fetch

And finally import both libraries into our code

import 'whatwg-fetch';
import 'babel-polyfill';
import {init} from './init';

init();

src/main.js

When we now build the app, it should even work on IE11.

To optimize the build we should specify the target platforms and only import polyfills and helper methods the application depends on. For that we change the settings in .babelrc

{
   "presets": [
     [ "env", {
       "targets": {
         "browsers": "last 2 versions, > 1%"
       },
       "debug": true,
       "useBuiltIns": true
     }]
   ]
}

Because you no longer need a .babelrc file with Parcel 1.6.x, you can configure the targeted browsers in the package.json file.

  "browserslist": [
    "> 1%",
    "last 2 versions"
  ]

The browserlist query string specifies what browser the project targets. In this example we target the last 2 versions of each browser and in addition all browsers with a market share of over 1 %. If you want to know what browsers this query matches, run the command

npx browserslist "last 2 versions, > 1%"

Or go to the https://browserl.ist website and enter the query string there. See also the browserslist project site to learn more about the supported queries.


Autoprefixer

Another common tool, projects use in their build process, is Autoprefixer. When browser vendors implement CSS features that are not mature they hide them behind a prefix (-webkit-, -moz-, -ms-) and when the CSS feature is stable the prefix gets removed in newer versions of the browsers. But when we target older browsers we need to add the prefixes where they are needed.

We install the Autoprefixer with this command

npm install autoprefixer -D

and then create the file .postcssrc and add this configuration.

{
  "plugins": {
    "autoprefixer": {
      "browsers": "last 2 versions, > 1%"
    }
  }
}

.postcssrc

We specify here the same targets that we set for the Babel transformation. When you build the application and open the CSS file in the dist folder you see that the Autoprefixer added the -webkit-box-shadow rule, a feature that was prefixed in older browsers.


Sass

Parcel also supports Sass and transforms it into CSS during the build process. You only have to install node-sass

npm install node-sass -D

You can now write Sass code

$font-stack:    Helvetica, sans-serif;
$primary-color: #333;

body {
 font: 100% $font-stack;
 color: $primary-color;
}

src/body.scss

and reference it in index.html

<link rel="stylesheet" href="./body.scss"/>

or import it into a css file. This is the better approach here because Parcel combines both files into one CSS file. The link tag approach creates two CSS files.

@import './body.scss';

Code Splitting

A common technique to reduce the size of the initial application bundle is to extract code that is not needed at the start of the application into separate modules. Parcel supports code splitting out of the box without any configuration. Parcel also manages the loading of the module when the application needs it.

To demonstrate this we add a button to our application and when clicked a dialog is displayed with the Tingle.js library (npm install tingle.js).

<button id="aboutButton">About</button>

src/index.html

   document.getElementById('aboutButton').addEventListener('click', async() => {
       const module = await import('./about');
       module.showAbout();
   });

src/init.js

Parcel uses the dynamic import() function that returns a Promise to control the code splitting.

import tingle from 'tingle.js';

export function showAbout() {
   const modal = new tingle.modal();
   modal.setContent('<h1>About me</h1>');
   modal.open();
}

src/about.js

In this example Parcel automatically creates a separate JavaScript file with the code in about.js. After the build process you will find two JavaScript files in the dist folder and you can observe the dynamic loading of the module in the browser developer tools when you click the About button.

Tingle requires some CSS, this cannot be loaded dynamically. We import this CSS file directly from the node_modules folder into main.css.

@import '../node_modules/tingle.js/src/tingle.css';

src/main.css


TypeScript

Parcel also supports TypeScript out of the box without any additional installation and configuration.

This application demonstrates the TypeScript support. All you need to do is reference the main TypeScript file in the index.html page.

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>TypeScript Example</title>
</head>
<body>
<div id="output">
</div>

<script src="./index.ts"></script>
</body>
</html>

src/index.html

import 'whatwg-fetch';
import "core-js/modules/es6.promise";
import "core-js/modules/es7.string.pad-start";
import numberURL from "./config";

async function fetchNumber(): Promise<void> {
   const response = await fetch(numberURL + Math.floor(Math.random() * 100));
   const txt = await response.text();
   document.getElementById('output').innerHTML = txt.padStart(100, '-');
}

fetchNumber();

src/index.ts

export default "http://numbersapi.com/";

src/config.ts

When you start the bundling process with npm run build, Parcel automatically installs the TypeScript compiler if it's not already installed (npm install typescript -D) and compiles the code into JavaScript.

You can configure the TypeScript compiler the usual way with a tsconfig.json file in the root of the project.

Note that the TypeScript compiler by default emits ES5 code but it does not automatically include polyfills. The example uses the Fetch API and the String.padStart method that was introduced in ES2017. So this example would not run on IE11.

To solve that we install the necessary polyfills

npm install whatwg-fetch
npm install core-js

and import them into the application

import 'whatwg-fetch';
import "core-js/modules/es6.promise";
import "core-js/modules/es7.string.pad-start";

Alternatively we can use a service like polyfill.io and only download polyfills that the browser needs. Instead of the polyfill imports, add this script tag to the html page.

<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Promise,fetch,String.prototype.padStart"></script>

This keeps the application bundle small but requires one additional http request.