How to configure ESBuild
Today, I wanted to share how I configure esbuild to create bundled both JavaScript and CSS files for my projects. Let’s dig in!
esbuild vs. other options
I recently added pre-bundled CSS and JavaScript files to Kelp, my UI library for people who love HTML.
In years past, I’ve reached for Rollup.js for this task. But configuring it has gotten more complicated in the latest version, it requires a bunch of plugins to do various tasks, it runs kind of slow, and because it includes so many dependencies, I’d find myself updating things a lot more than I want to.
So for Kelp, I decided to finally give esbuild a try.
My initial observations after a few weeks of using it…
- It’s absurdly fast. So fast you almost don’t believe it had time to run!
- It has a lot more stuff baked in. There are still plugins, but I’m not using any.
- The docs around configuration are a bit confusing, but configuration itself is really simple.
Today, I wanted to address that last point, and show you how I’ve got esbuild configured.
If this is something you’d like help with for your projects, I offer consulting around frontend architecture and performance. Feel free to book some time on my calendar so we can chat about what you’re work on.
Step 1: Install
Step zero is to make sure you have Node.js installed and a package.json file in your project directory.
npm initAfter that, step one is to install esbuild for your project.
npm install --save-exact --save-dev esbuildCreate a build script
You can run esbuild completely from the command line, but it’s easier if you create a JavaScript file to run stuff.
I created a file named esbuild.mjs (I use the .mjs extension because ES imports in Node require it). In it, I import esbuild and my package.json file.
import * as esbuild from 'esbuild';
import pkg from './package.json' with { type: 'json' };
I use the imported package.json file to create a banner that I’ll add to the top of my bundled files.
It adds the package name, version, author info, and URL dynamically, so the version gets automatically bumped with each update.
const banner = `/*! ${pkg.name} v${pkg.version} | (c) ${pkg.author.name} | ${pkg.repository.url} */`;
And back in my package.json script, I setup a build script that runs ./esbuild.mjs.
"scripts": {
"build": "node ./esbuild.mjs"
}When it’s time to run my build, I can type npm run build in the terminal, and it will run the esbuild.mjs script.
Configuring the build
Now for the fun stuff: configuring the actual build.
Here’s the annotated version…
// Run a build
await esbuild.build({
// A list of files to bundle
entryPoints: [
// Files to bundle...
],
// The root directory for the input files
outbase: 'src',
// The root directory for the output files
outdir: 'dist',
// The banner to use for JS and CSS files
banner: {
js: banner,
css: banner,
},
// If true, bundle files
bundle: true,
// If true, right the new file to the harddrive
// (otherwise, it's stored as a glob that you can do other stuff with)
write: true,
});
And here’s what this might look like in practice…
await esbuild.build({
entryPoints: [
'src/js/*.js',
'src/css/*.css',
],
outbase: 'src',
outdir: 'dist',
banner: {
js: banner,
css: banner,
},
bundle: true,
write: true,
});
The result
If you had a CSS file like this…
/* src/css/kelp.css */
/* Cascade Layers */
@import "./layers/layers.css";
/* Theme Files */
@import "./theme/fonts.css";It would spit out a file like this…
/* dist/css/kelp.css */
/*! kelpui v0.14.8 | (c) Chris Ferdinandi | http://github.com/cferdinandi/kelp */
/* modules/css/layers/layers.css */
@layer kelp;
@layer kelp.base, kelp.theme, kelp.palette, kelp.core, kelp.extend, kelp.layout, kelp.utilities, kelp.tokens, kelp.overrides, kelp.state, kelp.effects;
/* modules/css/theme/fonts.css */
@layer kelp.base {
:where(:root) {
--font-primary:
ui-sans-serif,
system-ui,
sans-serif;
--font-secondary:
Charter,
"Bitstream Charter",
"Sitka Text",
Cambria,
serif;
--font-monospace:
ui-monospace,
"Cascadia Code",
"Source Code Pro",
Menlo,
Consolas,
"DejaVu Sans Mono",
monospace;
}
}It does similar things with your JavaScript files and ESM import files.
What about minifying?
I’ve stopped minifying my code, but if you want to, esbuild makes this really easy, too!
If you want to minify as aggressively as possible, set minify to true and you’re all set.
// Run a build
await esbuild.build({
// ...
// If true, bundle files
bundle: true,
// If true, right the new file to the harddrive
// (otherwise, it's stored as a glob that you can do other stuff with)
write: true,
// Minify everything
minify: true,
});
esbuild also lets you have more fine-grained control over the minification process, if you’d prefer…
minifyWhitespace- removes all of the whitespaceminifyIdentifiers- changes variable and function names to shorter versions (const userFirstName = 'Chris'becomesvar x = 'Chris')minifySyntax- shortens syntax where possible (const double = (num) => { return num * 2 }becomesconst double = num => num * 2)
Setting minify: true is the equivalent of setting all three of these to true, but you can mix-and-match them as desired.
For example, you might want to remove whitespace and shorten the syntax where possible, but leave variables and functions untouched for easier debugging.
// Run a build
await esbuild.build({
// ...
// If true, bundle files
bundle: true,
// If true, right the new file to the harddrive
// (otherwise, it's stored as a glob that you can do other stuff with)
write: true,
// Minify SOME stuff
minifyWhitespace: true,
minifySyntax: true,
});
More features
esbuild has watch mode, which will automatically rebuild files when they’re changed. You can change the output format from it’s default of iffe to esm if you want to bundle files but run ESM natively.
If you have Typescript files, esbuild can transform them to JS for you.
It does a lot. The docs are worth exploring if you haven’t already.
And if you could use help setting up your build system, get in touch! I can help you integrate esbuild, automated testing, linting and type checking (with or without Typescript), CI integrations, and more.