Over the last few weeks I took some time to refactor the accessible menu library that I wrote. It had been a while since I'd worked on it and since I'd learned some better JS techniques I figured it was time.

As part of that re-build, I updated how the code was minified and bundled. But, that caused an unexpected problem that led me to learn a new thing!

At this point, for the sake of brevity, I'm going to assume you know about bundling code with a tool like browserify, parcel, or webpack. If you don't that's ok. There are a lot of resources to help you learn. Moving on.....

Usually in my projects and at work, I use either gulp with webpack or webpack by itself to bundle assets. That way we can write things like

// index.js
import Navigation from 'a11y-menu'

const navigation = new Navigation()
navigation.init()

But some of our projects rely on scripts being placed in above the closing body tag and then going from there.

<script src="/path/to/a11y-menu.js"></script>
<script src="/js/index.js"></script>
// index.js

const navigation = new Navigation()
navigation.init()

Unfortunately... my updates had inadvertently broken the ability to use the second method. I could still bundle the exported Navigation class in projects. But with the second method, Navigation was undefined. That's because when you use the default settings for webpack (and especialy when you minify things) you get code that looks like

function(t){var e={} // lots more code...}

Nowhere is there any mention of Navigation because it's been turned into... something?

Fortunately what I learned was that webpack has the ability to do exactly this! In the end it required a small adjustment to the output object of my webpack configuration.

Before I changed the webpack config file, I had a pretty standard entry/ouput setup like this -

const path = require('path')

module.exports = {
  entry: path.resolve(__dirname, 'src', 'js','Navigation', 'Navigation.js'),
  output: {
    filename: 'Navigation.js',
    path: path.resolve(__dirname, 'dist', 'Navigation.js'),
}

This setup would let you write like the first example

import Navigation from 'a11y-menu'

const navigation = new Navigation()

navigation.init()

To fix my issue, I looked into the settings for the plugins webpack uses to minify assets (e.g. terser). But that didn't really help. You can prevent terser from mangling class and function names. But that doesn't really help when the browser says the whole thing is undefined to begin with.

But with a careful reading of github issues and documentation, I found that what I wanted were the webpack output library properties! There are three of these

TLDR, these properties let me change the output object for webpack like this

module.exports = {
  entry: path.resolve(__dirname, 'src', 'js','Navigation', 'Navigation.js'),
  output: {
    filename: 'Navigation.js',
    path: path.resolve(__dirname, 'dist', 'Navigation.js'),
    // allows the Navigation class to be used as its own script
    library: 'Navigation',
    libraryTarget: 'var',
    libraryExport: 'default'
}

Let's explore what each of these does.

  • output.library defines the name of the variable that will be kept in the minified code.
  • output.libraryTarget sets how the library will be used. Since I want to use this in the browser, the easiest approach is to set it to 'var'.
  • output.libraryExport determines which of the library's modules will be exposed by the target. It's undefined by default. The code for the library in my case was under the default property so that's what I set this to.

With these settings in place, I was able to get minified code that looked like this.

var Navigation=function(t){var e={} // lots more code...}

From here, the Navigation class (now a function) would work as I wanted it to.

This was pretty interesting because I learned an important lesson about the difference between bundling assets for the browser and bundling for a library.