platformOS Documentation Site Webpack Setup
Tags: webperf, webpack, platformos
Published: 2020-12-04 18:25:30

Webpack is a powerful tool. Back in the day, it had a reputation of being hard to learn and hard to use. Nowadays, it has excellent documentation, sensible defaults, plugins, and loader - all this to help you keep your config small while achieving great results.

### Important features in 2020

The most important features of webpack for our projects are:

- Tree shaking
- Code splitting
- Dynamic async loading chunks of code

This article explains how we used webpack in our documentation for the past couple of years for easy development and performant production builds.

The final webpack config looks like this:

```bash
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const WebpackRequireFrom = require('webpack-require-from');
const webpack = require('webpack');

const production = process.env.NODE_ENV === 'production';

module.exports = {
  entry: {
    'app': './src/app',
    'graphql': './modules/graphql/public/assets/graphql',
  },
  output: {
    chunkFilename: '[name].[chunkhash:3].js',
    publicPath: '',
    path: path.resolve('app/assets'),
  },
  module: {
    rules: [
      {
        test: /(\.css)$/,
        use: [MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { url: false } }, 'postcss-loader'],
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        options: {
          exclude: /node_modules/,
          plugins: ['@babel/plugin-syntax-dynamic-import', '@babel/transform-object-assign'],
          cacheDirectory: true,
          presets: [
            [
              '@babel/preset-env'
            ]
          ]
        },
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: '[name].[chunkhash:3].css',
    }),
    new WebpackRequireFrom({
      variableName: 'window.__CONTEXT__.cdnUrl',
    }),
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify(process.env.NODE_ENV),
      },
    }),
  ],
  mode: production ? 'production' : 'development',
}
```

### Note about static assets

We do not process static assets with webpack. Fonts, images are just sitting already optimized in their proper place in our platformOS assets directory - app/assets. There are a couple of reasons we do that:

1. Manual optimization is usually better than automatic
2. Config is simpler
3. Fewer dependencies to break
4. Build is faster
5. We don't waste CPU time (= money) on CI doing the same operation on every build

## Technologies

**[PostCSS](https://postcss.org/)** is a framework for creating CSS parsing plugins. It allowed us to migrate away from SASS to reduce complexity. It handles some legacy browser fixes. It is also a base for TailwindCSS, a PostCSS plugin.

**[TailwindCSS](http://tailwindcss.com/)** is a CSS framework that makes it easy to create maintainable views with a minimal CSS footprint.

If you are interested in how we set up TailwindCSS and PostCSS, visit [our documentation GitHub repository](https://github.com/mdyd-dev/platformos-documentation/).

## Base config

Out of the box, webpack can work with no config at all. [Read more on prototyping with no config](https://auth0.com/blog/zero-config-javascript-app-prototyping-with-webpack/). By default, it looks for the `index.js` file in the `src/` directory and outputs to `dist/main.js`. We need to use a couple of features, so we need a config file. The webpack settings we use are pretty simple. 

### Entry points

```bash
entry: {
  'app': './src/app',
  'graphql': './modules/graphql/public/assets/graphql',
},
```

We use two entry points (files that we will pull in from our HTML) because we have completely separate CSS files for our documentation page and autogenerated documentation for GraphQL endpoints. `key` is to define how the output will be named, `value` is the relative path to the file webpack will treat as an entry point. For example: `app.js` will be generated from entry point `src/app.js`.

### Output

We don't define the output filename because by default, it is `[name].js`, and it works for our case. But for `chunkFilename`, we need to add chunkhash for cache invalidation purposes.

`publicPath` is set to empty because we will not use webpack's built in publicPath — we will use the one from `WebpackRequireFrom`, explained later.

path is the absolute path where the output files will be placed. In the case of platformOS, it is `app/assets`.

```bash
output: {
    chunkFilename: '[name].[chunkhash:3].js',
    publicPath: '',
    path: path.resolve('app/assets'),
  },
```

## Plugins

**[MiniCssExtractPlugin](https://github.com/webpack-contrib/mini-css-extract-plugin)** extracts CSS to external files. Without it, webpack would package everything into one JS and one CSS.

```bash
new MiniCssExtractPlugin({
    filename: '[name].css',
    chunkFilename: '[name].[chunkhash:3].css',
})
```

The platformOS Liquid filter for getting the URL to assets placed on the CDN adds the query param to bust the cache, so the main files that we require in Liquid do not need to vary in between different builds. We add `chunkhash:3` in the file name for chunks because those are loaded dynamically from webpack, and there won't be a query param to handle that.

Why all that? To prevent browsers from loading a file from cache (old asset could break something) if it changed.

Files loaded using the native platformOS `asset_url` filter:

![asset_url cache bust](https://dev-to-uploads.s3.amazonaws.com/i/8nr7km8tm2h6m0g2xnc6.png)


Chunks loaded with chunkhash in the filename:

![chunks filename cache bust](https://dev-to-uploads.s3.amazonaws.com/i/1l265an5z4adyryqxnlp.png)


**[WebpackRequireFrom](https://github.com/agoldis/webpack-require-from)** sets the CDN URL during runtime for dynamic chunks, so we don't have to hardcode anything. We just pull this URL from the HTML source, which is rendered server-side.

```bash
new WebpackRequireFrom({
    variableName: 'window.__CONTEXT__.cdnUrl',
}),
```

This option tells webpack where to look to get the CDN URL in the browser when the application is running. In Liquid, we define `window.__CONTEXT__.cdnUrl` to our CDN URL to let webpack know where to look for dynamically loaded chunks.

```bash
<script>
  window.__CONTEXT__ = { cdnUrl: "{{ '' | asset_url }}" };
</script>
```

This results in chunks loaded correctly on all environments provided by platformOS: dev, staging, and production.

The **[webpack.DefinePlugin](https://webpack.js.org/plugins/define-plugin/)** allows us to pass build mode (production or dev) to the runtime. This is needed by the Algolia search script that we use as our search engine.

```bash
new webpack.DefinePlugin({
 'process.env': {
     NODE_ENV: JSON.stringify(process.env.NODE_ENV),
  },
}),
```

Keep in mind that for this line to work, we execute webpack-cli with NODE_ENV defined in the npm script.

```bash
"build": "NODE_ENV=production npx webpack-cli --no-color",
```

## Loaders

Loaders are specific plugins for webpack that extend its functionality to operate on files that it does not understand by default. By default, webpack can only parse and bundle JavaScript.

The order of loaders matters. For example, css-loader cannot parse syntax written for PostCSS, so PostCSS needs to be executed first. Loaders are executed from right to left.

postcss-loader allows webpack to use the PostCSS plugins ecosystem. It usually needs the postcss.config.js file to add PostCSS plugins because PostCSS on its own doesn't do anything.

```bash
{
    test: /(\.css)$/,
    use: [
			MiniCssExtractPlugin.loader,
			{ loader: 'css-loader', options: { url: false } },
			'postcss-loader'
		],
},
```

For CSS files, PostCSS will do its thing first, pass the output to css-loader, it will do its thing (without resolving URLs in CSS) and pass the output to MiniCssExtractPlugin.loader which will save the CSS to separate files.

[babel-loader](https://github.com/babel/babel-loader) transpiles JavaScript using babel and its plugins. We use `preset-env` and two plugins: `syntax-dynamic-import` and `transform-object-assign` for browsers defined in [package.json](https://github.com/mdyd-dev/platformos-documentation/blob/1505cf2c7ace239d46abec979f2c01f0b4d5bd1b/package.json#L27).

## Code splitting and conditional loading

This article wouldn't be complete if I did not include how to split code in webpack build time so that it asynchronously loads chunks while running in the browser.

```bash
import './app.css';

import { $q } from './js/helpers/dom';

import './js/sidebarMenu';
import './js/deepLinks';
import './js/autosteps'; // this HAS to be after deepLinks
import './js/toc';
import './js/externalLinks';
import './js/feedback';

import(/* webpackChunkName: "search" */ './js/search');

if ($q('code[class*="language-"]')) {
  import(/* webpackChunkName: "syntaxHighlighting" */ './js/syntaxHighlighting');
}
```

- All the ES6 imports (from `sidebarMenu` to `feedback`) will be bundled directly into app.js.
- Dynamic imports (function named import - `search`) will generate chunks, that will be loaded asynchronously.
- Dynamic imports wrapped in an `if` statement will be loaded asynchronously only when the `if` condition evaluates to a `truthy` value. In our case, when a certain selector is present on the webpage. Webpack will not load those modules on pages that do not require them.

Example requests for the homepage:

![Conditional loading - no syntax highlighting](https://dev-to-uploads.s3.amazonaws.com/i/trfr3c1kt07u7jtb7wla.png)

Example requests for a page where syntax highlighting is needed:

![Conditional loading - with syntax highlighting](https://dev-to-uploads.s3.amazonaws.com/i/fhabisiigwglhyq3nr87.png)

### Closing thoughts

Webpack in 2020 is a totally different beast than it was in 2015, but it can still be intimidating. If you don't feel its syntax, you can try different code bundlers; some of them are simpler to some people ([rollup](https://www.rollupjs.org/)), and some are much faster ([esbuild](https://esbuild.github.io/)). We chose webpack a long time ago because it was reliable, the most feature-rich (thanks to plugins), powerful, and very well documented.

If you are interested in a Webpack + TailwindCSS setup for static websites (with HTML that you can deploy to platformOS, netlify, vercel, or just AWS S3), we created an [opensource boilerplate](https://github.com/pavelloz/webpack-tailwindcss-purgecss) for you to quickly jump in and test it out.