Home | Send Feedback

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 configurations 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 generate package.json with the command npm init -y. Next create a subdirectory src, that will host all the source files. Then copy the files from this GitHub repository into the src folder: 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 an 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 the latter approach, because setting up a project just requires a git clone and a npm install. A new team member does not have to worry about global installed npm packages.

When you install 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 an 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, out of the box, always transpiles the JavaScript code with Babel, bundles 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 does all this without any configuration.

Calling Parcel with npx gets a bit tedious over time so my preferred solution is to add 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

    "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 to set for example URLs that are specific for this 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 variables from a .env file into process.env.

For example, you have two environments development and production and you want to set some variables that are different for each environment. 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;

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 add 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 by default 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 a web server.

parcel watch src/index.html

Babel

As mentioned before, Parcel automatically transpiles all the JavaScript code with Babel to code that runs on older browsers like IE11.
Our example uses features like async/await (ES2017), arrow functions, let/const, template strings (ES2015) and the packaged application runs fine on IE11.

One thing we have to do is adding the @babel/polyfill package because the code that Babel generates depends on functions from this package

npm install @babel/polyfill

and then import the package in src/main.js

import '@babel/polyfill';

You can configure Babel the usual way by adding a .babelrc file to the project. Babel transpiles the code with @babel/preset-env and for the browser target it uses browserlist.

In this example I configured the browser targets in the package.json file.

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

package.json

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.

Visit the official Parcel documentation page for more information about Babel configuration:
https://parceljs.org/javascript.html#default-babel-transforms


With Babel, we can use new JavaScript constructs in our code and still target older browsers but it does not solve the problem of missing browser APIs. In this example I use the Fetch API, which is not implemented in IE11. Fortunately for a lot of browser features polyfills exists, that implement an API in older browsers.

In case of the Fetch API we add the whatwg polyfill to our project.

npm install whatwg-fetch

And import it into our code

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

init();

main.js

When we now build the app, it should run fine on IE11.

Autoprefixer

Another common tool, projects use in their build process, is Autoprefixer.
When browser vendors implement CSS features that are not mature enough 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 create the file .postcssrc in the root of the project 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 a -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 can write SASS code

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

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

body.scss

and reference it from a HTML file

<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 separate CSS files.

@import './body.scss';

For the SASS compilation, Parcel depends on the sass or node-sass package. You can either install them with npm install -D ... or let Parcel do it automatically the first time it builds the project.

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>

index.html

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

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();
}

about.js

In this example Parcel automatically creates a separate JavaScript file with the code from 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 depends on a CSS file, this cannot be loaded dynamically. We therefore import this CSS file directly from the node_modules folder into main.css.

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

main.css

TypeScript

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

The following 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="https://cdn.polyfill.io/v2/polyfill.min.js?features=Promise,fetch,String.prototype.padStart"></script>
-->

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

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();


index.ts

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

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 transpiles 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 even the emitted JavaScript code is based on ES5.

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 installing the two polyfills and import them, 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 to an external service.

If you are interested in polyfill.io and want to host it on your own server, check out my blog post about this topic: https://golb.hplar.ch/2018/01/Self-host-Polyfill-io.html