A look at esbuild

2022-02-05

I've recently made a new small web project which requires some Javascript. I did try initially to make it as simple as possible using only vanilla JS but eventually making use of some libraries became necessary, for both code quality and things like animation. Libraries and bundling require build tooling and the Javascript ecosystem has far too much choice in that front. From experience at work and some other projects I've used both Webpack and Rollup but I wanted to try something different that I had heard good things about: esbuild.

esbuild is interesting in that it is very "batteries included" compared to most other JS build tools. Its default install is capable of module path resolving, bundling, tree shaking, minification, and sourcemaps all without the needing to install plugins. Additional perks include an in-built dev server, support for JSX, and support for Typescript. The main party trick is that it is fast! Unlike most JS build tools, esbuild is not written in Javascript but in Go and is distributed as a statically compiled binary executable with an optional Javascript API. As a result, a single command line invocation of esbuild can replace a more complex Webpack config with no additional plugins, and still be much faster.

As an example here's the command I used to use for development on my current project and a breakdown of what each part does:

Shell code
$ esbuild src/app.js --outfile=./www/static/main.js --bundle --sourcemap --servedir=./www

As another example here's the command I used to use for building for deployment with different flags and a breakdown of what each part does:

Shell code
$ esbuild src/app.js --outfile=./www/static/main.js --bundle --minify --legal-comments=none --drop:console --analyze

esbuild having all its options available via command line flags is quite handy, especially when iterating and trying to find the correct setting for your project. Even in the documentation, the command line flags are always presented as the first choice for any configurable option. As such, you can just easily just copy these command line invocations into your package.json scripts section and have things up and running really quickly. Here's what that used to look like for my project:

package.json
{
  "scripts": {
    "dev": "esbuild src/app.js --outfile=./www/static/main.js --bundle --sourcemap --servedir=./www",
    "build": "esbuild src/app.js --outfile=./www/static/main.js --bundle --minify --legal-comments=none --drop:console --analyze"
  }
}

While these lines aren't the most unwieldy, they aren't the best to look at and as with most tooling I wanted to move this to a config file that esbuild could use instead. Unfortunately, one of esbuild's drawbacks as it does not support using a config file. Instead esbuild chooses to expose a Javascript API to invoke the core process so that you can write a script which can be used in place of a config file. While this does indeed cover the use case of having the configuration stored in it's own file, using the API has the side effect of disabling the default console output which is something I found really useful. As such, I ended up making a simple script to read in a simple JSON config file, parse it into the expected command line flags and then pipe the result into the esbuild binary. As a result, my package.json file and associated esbuild JSON config files now look like this:

package.json
{
  "scripts": {
    "build": "./bin/esbuildParse.mjs ./esbuild.build.json | xargs esbuild",
    "dev": "./bin/esbuildParse.mjs ./esbuild.dev.json | xargs esbuild",
  }
}
esbuild.dev.json
{
  "entry": "./src/app.js",
  "outfile": "./www/static/main.js",
  "bundle": true,
  "sourcemap": true,
  "servedir": "./www"
}
esbuild.build.json
{
  "entry": "./src/app.js",
  "outfile": "./www/static/main.js",
  "bundle": true,
  "minify": true,
  "legal-comments": "none",
  "drop": "console",
  "analyze": true
}

Looking at npm, there do seem to be packages that provide this sort of functionality to wrap the esbuild command with a config file, but I think I will just stick with my simple script for now.