How to build a webpack assets loader script
Published: 2020-12-06 15:55:21

Sometimes we need to load our webpack bundled JavaScript app into a host application that we don't have access to the HTML page's `<head>` or the capability to provide our own `index.html`. Scripts and CSS are usually loaded dynamically and injected into the page by the host application at runtime. We only get a location to upload the assets of our app, a place to define the JavaScript files that need loading, and a `div` id in which our app will mount.

Now, every time our project is built for production, webpack generates new chunk ids for the output files. In a common scenario, usually, these are inserted automatically into the `index.html` template file with HtmlWebpackPlugin. In this given scenario though since we don't have any access to the `index.html`, we have to manually define in the host application the JS and CSS files needed for our app to load. Things get more complicated if our bundle is split into multiple initial chunks (which it should). 

Let's see how we can create a webpack assets loader script that will automate the process of loading all JS and CSS assets with their correct chunk ids and display a nice progress bar while doing so. We'll use webpack 5 but the setup is similar for webpack 4 too.

### Part 1 - the loader
First, let's start by creating a folder named `loader` into our project's `src` folder that will contain all the necessary files. We're gonna need a JavaScript loader that we'll actually fetch the JS and CSS using XHR and inject them into the page. There are a lot of solutions floating around the web but we are going to create a super simple one using this excellent [post](https://davidwalsh.name/javascript-loader) by David Walsh as a base.
`src/loader/assets-loader.js`

```javascript
const filetypeToTag = {
  css: 'link',
  js: 'script'
}

export function load (url) {
  const filetype = url.split('.').pop()
  const tag = filetypeToTag[filetype]
  return new Promise((resolve, reject) => {
    const element = document.createElement(tag)
    let parent = 'body'
    let attr = 'src'
    element.onload = () => {
      resolve(url)
    }
    element.onerror = () => {
      reject(url)
    }
    switch (tag) {
      case 'script':
        element.async = false
        break
      case 'link':
        element.type = 'text/css'
        element.rel = 'stylesheet'
        attr = 'href'
        parent = 'head'
    }
    element[attr] = url
    document.head.appendChild(element)
  })
}
```
We can then simply load a JS or CSS asset with:
```javascript
load('example.js').then(() => {
  // asset is now loaded
})
```

### Part 2 - the progressbar
Next, we are going to create our HTML template for the loading animation that will display while the assets are downloading. This will consist of an image (that could be the branding image of our website) and an animating progress bar.
`src/loader/loader.html`
```html
<style type="text/css">
  #loader {
    position: fixed;
    background-color: #fff;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 9999;
    display: flex;
    justify-content: center;
  }
  #loader .container {
    top: 30%;
    position: relative;
  }
  #loader .image {
    background:url('{{logoLoader}}');
    width: 130px;
    background-size: 100%;
    height: 141px;
    margin: 0px auto 40px auto;
    opacity: 0;
    animation: loader-fade 0.25s ease-in-out forwards;
  }
  #loader .progress {
    width: 300px;
    height: 8px;
    overflow: hidden;
    background-color: #ddd;
    margin: 0 auto 0 auto;
    text-align: center;
    border-radius: 10px;
    opacity: 0;
    animation: loader-fade 0.7s ease-in-out forwards;
  }
  #loader .progress .progress-bar {
    width: 0%;
    max-width: 100%;
    height: 8px;
    overflow: hidden;
    position: relative;
    background-color: #1c78c0;
    animation-name: loader-progress;
    animation-duration:  20s;
    animation-timing-function: cubic-bezier(0.024, 0.9, 0.024, 0.9);
    animation-delay: 0.5s;
    animation-iteration-count: 1;
    animation-fill-mode: forwards;
    border-radius: 10px;
  }
  #loader .progress .progress-bar .shimmer  {
    position: absolute;
    left: -400px;
    top: 0px;
    animation-name: loader-shimmer;
    animation-duration: 1.1s;
    animation-timing-function: linear;
    animation-delay: 0.65s;
    animation-iteration-count: infinite;
    z-index: 10;
  }
  #loader .progress .progress-bar .shimmer > div {
    position: absolute;
    width: 400px;
    height: 8px;    
    background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.35) 45%, rgba(255,255,255,0.45) 60%, rgba(255,255,255,0.55) 75%, rgba(255,255,255,0) 100%);
  }
  @keyframes loader-progress {
    to {
      width: 95%
    }
  }
  @keyframes loader-shimmer {
    0% {
      transform: translateX(-50px);
      animation-timing-function: cubic-bezier(0.85, 0, 0.64, 1);
    }
    98.36% {
      transform: translateX(600px);
      animation-timing-function: linear;
    }
    100% {
      transform: translateX(600px);
    }
  }
  @keyframes loader-fade {
    to {
      opacity: 1;
    }
  }
</style>

<div id="loader">
  <div class="container">
    <div class="image"></div>
    <div class="progress">
      <div class="progress-bar">
        <div class="shimmer">
          <div></div>
        </div>
      </div>
    </div>
  </div>
</div>
```
We are later going to replace `{{logoLoader}}` with a base64 string of our image. You can view the final result of the progress bar animation in this codesandbox [here](https://codesandbox.io/s/loader-progress-bar-0t1ur?file=/index.html:1602-1621).

### Part 3 - webpack configuration
Now for the tricky part. We need to get all the built files emitted by webpack in order to feed them into our loader. [HtmlWebpackPlugin](https://github.com/jantimon/html-webpack-plugin) to the rescue! Although usually HtmlWebpackPlugin is used to generate an HTML file, we're going to use HtmlWebpackPlugin's template option to generate a JavaScript file instead. That will export the list of webpack build files as an Array.
If not already present inside your webpack build config, you can follow the instructions [here](https://github.com/jantimon/html-webpack-plugin#install) to get it installed. 

Ok, now for some changes in our `webpack.config.js`. We' are going to create a separate webpack config for our loader so we'll need to alter the project's webpack config to incorporate [multiple configs](https://webpack.js.org/configuration/configuration-types/#exporting-multiple-configurations). So basically now you'll have your webpack config as an array of objects rather than a single object.

Inside our existing app config, we're going to add a section for HtmlWebpackPlugin. In our loader config we'll have `src/loader/loader.js` as an entry point.

`webpack.config.js`
```javascript
...
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = [
  // App config here...
  {
    entry: ' ... ',
    output { ... },
    plugins: [ 
    ...
      new HtmlWebpackPlugin({
        template: 'src/loader/files-template.js',
        filename: '../src/loader/files.js',
        inject: false,
        publicPath: ''
      })
    ],
    ...
  },
  // Loader config here...
  {
    entry: './src/loader/loader.js',
    output: {
      filename: 'loader.js',
      path: path.resolve(__dirname, 'dist')
    },
    module: {
      rules: [
        {
          test: /\.html$/i,
          loader: 'html-loader'
        },
        {
          test: /\.png/,
          type: 'asset/inline'
        }
      ]
    }
  }
]
```
The html-loader plugin is used so that we can import our progress bar template HTML file as a string. We are using the assets module feature of webpack 5 to have our logo image imported as a base64 string. You can achieve the same result in webpack 4 using [url-loader](https://webpack.js.org/loaders/url-loader/).

Let's now create the HtmlWebpackPlugin template file. Create a new file `files-template.js` and put the following inside
`src/loader/files-template.js`
```javascript
module.exports = templateParams => {
  const files = templateParams.htmlWebpackPlugin.files
  const assets = files.js.concat(files.css)
  return `export default ${JSON.stringify(assets)}`
}
```
This will generate a file (`/src/loader/files.js` as defined previously in our config) that will export all JS and CSS assets of the webpack build as an Array.

### Part 4 - Putting it all together
Finally, let's create our loader entry point that will be used to inject the loading animation into the page (into an `#app` div) and load the webpack assets.
`src/loader/files-template.js`
```javascript
import files from './files.js'
import { load } from './assets-loader.js'
import logoLoader from './logo-loader.png'
import template from './loader.html'

document.addEventListener('DOMContentLoaded', () => {
  document.querySelector('#app').insertAdjacentHTML('beforeend', template.replace('{{logoLoader}}', logoLoader))

  Promise.all(files.map(o => load(o)))
})
```

That's it! Now when we run our build (usually with `npm run build`) a new file `loader.js` will be generated inside our `dist` directory. That's the only `script` we need to load our app!
`loader.js` can even be injected dynamically into the page and our app will load.

You can find an example [GitHub repo](https://github.com/kouts/webpack-assets-loader) with all the source code as well as a [live demo here](https://webpack-assets-loader.netlify.app/).
Have fun loading!