An Introduction to the esbuild Bundler — SitePoint

esbuild is a fast bundler that can optimize JavaScript, TypeScript, JSX, and CSS code. This article will help you get up to speed with esbuild and show you how to create your own build system without other dependencies.

Table of Contents
  1. How Does esbuild Work?
  2. Why Bundle?
  3. Why Use esbuild?
  4. Why Avoid esbuild?
  5. Super-quick Start
  6. Example Project
  7. Project Overview
  8. Configuring esbuild
  9. JavaScript Bundling
  10. CSS Bundling
  11. Watching, Rebuilding, and Serving
  12. Summary

How Does esbuild Work?

Frameworks such as Vite have adopted esbuild, but you can use esbuild as a standalone tool in your own projects.

  • esbuild bundles JavaScript code into a single file in a similar way to bundlers such as Rollup. This is esbuild’s primary function, and it resolves modules, reports syntax issues, “tree-shakes” to remove unused functions, erases logging and debugger statements, minifies code, and provides source maps.
  • esbuild bundles CSS code into a single file. It’s not a full substitute for pre-processors such as Sass or PostCSS, but esbuild can handle partials, syntax issues, nesting, inline asset encoding, source maps, auto-prefixing, and minification. That may be all you need.
  • esbuild also provides a local development server with automatic bundling and hot-reloading, so there’s no need to refresh. It doesn’t have all the features offered by Browsersync, but it’s good enough for most cases.

The code below will help you understand esbuild concepts so you can investigate further configuration opportunities for your projects.

Why Bundle?

Bundling code into a single file offers various benefits. Here are some of them:

  • you can develop smaller, self-contained source files which are easier to maintain
  • you can lint, prettify, and syntax-check code during the bundling process
  • the bundler can remove unused functions — known as tree-shaking
  • you can bundle alternative versions of the same code, and create targets for older browsers, Node.js, Deno, and so on
  • single files load faster than multiple files and the browser doesn’t require ES module support
  • production-level bundling can improve performance by minifying code and removing logging and debugging statements

Why Use esbuild?

Unlike JavaScript bundlers, esbuild is a compiled Go executable which implements heavy parallel processing. It’s quick and up to one hundred times faster than Rollup, Parcel, or Webpack. It could save weeks of development time over the lifetime of a project.

In addition, esbuild also offers:

  • built-in bundling and compilation for JavaScript, TypeScript, JSX, and CSS
  • command-line, JavaScript, and Go configuration APIs
  • support for ES modules and CommonJS
  • a local development server with watch mode and live reloading
  • plugins to add further functionality
  • comprehensive documentation and an online experimentation tool

Why Avoid esbuild?

At the time of writing, esbuild has reached version 0.18. It’s reliable but still a beta product.

esbuild is frequently updated and options may change between versions. The documentation recommends you stick with a specific version. You can update it, but you may need to migrate your configuration files and delve into new documentation to discover breaking changes.

Note also that esbuild doesn’t perform TypeScript type checking, so you’ll still need to run tsc -noEmit.

Super-quick Start

If necessary, create a new Node.js project with npm init, then install esbuild locally as a development dependency:

<code class="bash language-bash"><span class="token function">npm</span> <span class="token function">install</span> esbuild --save-dev --save-exact
</code>

The installation requires around 9MB. Check it works by running this command to see the installed version:

<code class="bash language-bash">./node_modules/.bin/esbuild --version
</code>

Or run this command to view CLI help:

<code class="bash language-bash">./node_modules/.bin/esbuild --help
</code>

Use the CLI API to bundle an entry script (myapp.js) and all its imported modules into a single file named bundle.js. esbuild will output a file using the default, browser-targeted, immediately-invoked function expression (IIFE) format:

<code class="bash language-bash">./node_modules/.bin/esbuild myapp.js --bundle --outfile<span class="token operator">=</span>bundle.js
</code>

You can install esbuild in other ways if you’re not using Node.js.

Example Project

Download the example files and an esbuild configuration from Github. It’s a Node.js project, so install the single esbuild dependency with:

<code class="bash language-bash"><span class="token function">npm</span> <span class="token function">install</span>
</code>

Build the source files in src to a build directory and start a development server with:

<code class="bash language-bash"><span class="token function">npm</span> start
</code>

Now navigate to localhost:8000 in your browser to view a web page showing a real-time clock. When you update any CSS file in src/css/ or src/css/partials, esbuild will re-bundle the code and live reload the styles.

Press Ctrl|Cmd + Ctrl|Cmd to stop the server.

Create a production build for deployment using:

<code class="bash language-bash"><span class="token function">npm</span> run build
</code>

Examine the CSS and JavaScript files in the build directory to see the minified versions without source maps.

Project Overview

The real-time clock page is constructed in a build directory using source files from src.

The package.json file defines five npm scripts. The first deletes the build directory:

<code class="javascript language-javascript"><span class="token string">"clean"</span><span class="token operator">:</span> <span class="token string">"rm -rf ./build"</span><span class="token punctuation">,</span>
</code>

Before any bundling occurs, an init script runs clean, creates a new build directory and copies:

  1. a static HTML file from src/html/index.html to build/index.html
  2. static images from src/images/ to build/images/
<code class="javascript language-javascript"><span class="token string">"init"</span><span class="token operator">:</span> <span class="token string">"npm run clean && mkdir ./build && cp ./src/html/* ./build/ && cp -r ./src/images ./build"</span><span class="token punctuation">,</span>
</code>

An esbuild.config.js file controls the esbuild bundling process using the JavaScript API. This is easier to manage than passing options to the CLI API, which can become unwieldy. An npm bundle script runs init followed by node ./esbuild.config.js:

<code class="javascript language-javascript"><span class="token string">"bundle"</span><span class="token operator">:</span> <span class="token string">"npm run init && node ./esbuild.config.js"</span><span class="token punctuation">,</span>
</code>

The last two npm scripts run bundle with either a production or development parameter passed to ./esbuild.config.js to control the build:

<code class="javascript language-javascript"><span class="token string">"build"</span><span class="token operator">:</span> <span class="token string">"npm run bundle -- production"</span><span class="token punctuation">,</span>
<span class="token string">"start"</span><span class="token operator">:</span> <span class="token string">"npm run bundle -- development"</span>
</code>

When ./esbuild.config.js runs, it determines whether it should create minified production files (the default) or development files with automatic updates, source maps, and a live-reloading server. In both cases, esbuild bundles:

  • the entry CSS file src/css/main.css to build/css/main.css
  • the entry JavaScript file scr/js/main.js to build/js/main.js

Configuring esbuild

package.json has a "type" of "module" so all .js files can use ES Modules. The esbuild.config.js script imports esbuild and sets productionMode to true when bundling for production or false when bundling for development:

<code class="javascript language-javascript"><span class="token keyword module">import</span> <span class="token imports"><span class="token punctuation">{</span> argv <span class="token punctuation">}</span></span> <span class="token keyword module">from</span> <span class="token string">'node:process'</span><span class="token punctuation">;</span>
<span class="token keyword module">import</span> <span class="token imports"><span class="token operator">*</span> <span class="token keyword module">as</span> esbuild</span> <span class="token keyword module">from</span> <span class="token string">'esbuild'</span><span class="token punctuation">;</span>

<span class="token keyword">const</span>
  productionMode <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token string">'development'</span> <span class="token operator">!==</span> <span class="token punctuation">(</span>argv<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span> <span class="token operator">||</span> process<span class="token punctuation">.</span><span class="token property-access">env</span><span class="token punctuation">.</span><span class="token constant">NODE_ENV</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
  target <span class="token operator">=</span> <span class="token string">'chrome100,firefox100,safari15'</span><span class="token punctuation">.</span><span class="token method function property-access">split</span><span class="token punctuation">(</span><span class="token string">','</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span> productionMode <span class="token operator">?</span> <span class="token string">'production'</span> <span class="token operator">:</span> <span class="token string">'development'</span> <span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> build</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code>

Bundle target

Note that the target variable defines an array of browsers and version numbers to use in the configuration. This affects the bundled output and changes the syntax to support specific platforms. For example, esbuild can:

  • expand native CSS nesting into full selectors (nesting would remain if "Chrome115" was the only target)
  • add CSS vendor-prefixed properties where necessary
  • polyfill the ?? nullish coalescing operator
  • remove # from private class fields

As well as browsers, you can also target node and es versions such as es2020 and esnext (the latest JS and CSS features).

JavaScript Bundling

The simplest API to create a bundle:

<code class="javascript language-javascript"><span class="token keyword control-flow">await</span> esbuild<span class="token punctuation">.</span><span class="token method function property-access">build</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
  entryPoints<span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'myapp.js'</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
  bundle<span class="token operator">:</span> <span class="token boolean">true</span>
  outfile<span class="token operator">:</span> <span class="token string">'bundle.js'</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code>

This replicates the CLI command used above:

<code class="bash language-bash">./node_modules/.bin/esbuild myapp.js --bundle --outfile<span class="token operator">=</span>bundle.js
</code>

The example project uses more advanced options such as file watching. This requires a long-running build context which sets the configuration:

<code class="javascript language-javascript">
<span class="token keyword">const</span> buildJS <span class="token operator">=</span> <span class="token keyword control-flow">await</span> esbuild<span class="token punctuation">.</span><span class="token method function property-access">context</span><span class="token punctuation">(</span><span class="token punctuation">{</span>

  entryPoints<span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token string">'./src/js/main.js'</span> <span class="token punctuation">]</span><span class="token punctuation">,</span>
  format<span class="token operator">:</span> <span class="token string">'esm'</span><span class="token punctuation">,</span>
  bundle<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
  target<span class="token punctuation">,</span>
  drop<span class="token operator">:</span> productionMode <span class="token operator">?</span> <span class="token punctuation">[</span><span class="token string">'debugger'</span><span class="token punctuation">,</span> <span class="token string">'console'</span><span class="token punctuation">]</span> <span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
  logLevel<span class="token operator">:</span> productionMode <span class="token operator">?</span> <span class="token string">'error'</span> <span class="token operator">:</span> <span class="token string">'info'</span><span class="token punctuation">,</span>
  minify<span class="token operator">:</span> productionMode<span class="token punctuation">,</span>
  sourcemap<span class="token operator">:</span> <span class="token operator">!</span>productionMode <span class="token operator">&&</span> <span class="token string">'linked'</span><span class="token punctuation">,</span>
  outdir<span class="token operator">:</span> <span class="token string">'./build/js'</span>

<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code>

esbuild offers dozens of configuration options. Here’s a rundown of the ones used here:

  • entryPoints defines an array of file entry points for bundling. The example project has one script at ./src/js/main.js.
  • format sets the output format. The example uses esm, but you can optionally set iife for older browsers or commonjs for Node.js.
  • bundle set to true inlines imported modules into the output file.
  • target is the array of target browsers defined above.
  • drop is an array of console and/or debugger statements to remove. In this case, production builds remove both and development builds retain them.
  • logLevel defines the logging verbosity. The example above shows errors during production builds and more verbose information messages during development builds.
  • minify reduces the code size by removing comments and whitespace and renaming variables and functions where possible. The example project minifies during production builds but prettifies code during development builds.
  • sourcemap set to linked (in development mode only) generates a linked source map in a .map file so the original source file and line is available in browser developer tools. You can also set inline to include the source map inside the bundled file, both to create both, or external to generate a .map file without a link from the bundled JavaScript.
  • outdir defines the bundled file output directory.

Call the context object’s rebuild() method to run the build once — typically for a production build:

<code class="javascript language-javascript"><span class="token keyword control-flow">await</span> buildJS<span class="token punctuation">.</span><span class="token method function property-access">rebuild</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
buildJS<span class="token punctuation">.</span><span class="token method function property-access">dispose</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 
</code>

Call the context object’s watch() method to keep running and automatically re-build when watched files change:

<code class="javascript language-javascript"><span class="token keyword control-flow">await</span> buildJS<span class="token punctuation">.</span><span class="token method function property-access">watch</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code>

The context object ensures subsequent builds are processed incrementally and that they reuse work from previous builds to improve performance.

JavaScript input and output files

The entry src/js/main.js file imports dom.js and time.js modules from the lib sub-folder. It finds all elements with a class of clock and sets their text content to the current time every second:

<code class="javascript language-javascript"><span class="token keyword module">import</span> <span class="token imports"><span class="token operator">*</span> <span class="token keyword module">as</span> dom</span> <span class="token keyword module">from</span> <span class="token string">'./lib/dom.js'</span><span class="token punctuation">;</span>
<span class="token keyword module">import</span> <span class="token imports"><span class="token punctuation">{</span> formatHMS <span class="token punctuation">}</span></span> <span class="token keyword module">from</span> <span class="token string">'./lib/time.js'</span><span class="token punctuation">;</span>


<span class="token keyword">const</span> clock <span class="token operator">=</span> dom<span class="token punctuation">.</span><span class="token method function property-access">getAll</span><span class="token punctuation">(</span><span class="token string">'.clock'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword control-flow">if</span> <span class="token punctuation">(</span>clock<span class="token punctuation">.</span><span class="token property-access">length</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>

  <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">'initializing clock'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token function">setInterval</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token punctuation">{</span>

    clock<span class="token punctuation">.</span><span class="token method function property-access">forEach</span><span class="token punctuation">(</span><span class="token parameter">c</span> <span class="token arrow operator">=></span> c<span class="token punctuation">.</span><span class="token property-access">textContent</span> <span class="token operator">=</span> <span class="token function">formatHMS</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token punctuation">}</span>
</code>

dom.js exports two functions. main.js imports both but only uses getAll():

<code class="javascript language-javascript">


<span class="token keyword module">export</span> <span class="token keyword">function</span> <span class="token function">get</span><span class="token punctuation">(</span><span class="token parameter">selector<span class="token punctuation">,</span> doc <span class="token operator">=</span> <span class="token dom variable">document</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword control-flow">return</span> doc<span class="token punctuation">.</span><span class="token method function property-access">querySelector</span><span class="token punctuation">(</span>selector<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>


<span class="token keyword module">export</span> <span class="token keyword">function</span> <span class="token function">getAll</span><span class="token punctuation">(</span><span class="token parameter">selector<span class="token punctuation">,</span> doc <span class="token operator">=</span> <span class="token dom variable">document</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword control-flow">return</span> <span class="token known-class-name class-name">Array</span><span class="token punctuation">.</span><span class="token keyword module">from</span><span class="token punctuation">(</span>doc<span class="token punctuation">.</span><span class="token method function property-access">querySelectorAll</span><span class="token punctuation">(</span>selector<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code>

time.js exports two functions. main.js imports formatHMS(), but that uses the other functions in the module:

<code class="javascript language-javascript">


<span class="token keyword">function</span> <span class="token function">timePad</span><span class="token punctuation">(</span><span class="token parameter">n</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword control-flow">return</span> <span class="token known-class-name class-name">String</span><span class="token punctuation">(</span>n<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token method function property-access">padStart</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">,</span> <span class="token string">'0'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>


<span class="token keyword module">export</span> <span class="token keyword">function</span> <span class="token function">formatHM</span><span class="token punctuation">(</span>d <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword control-flow">return</span> <span class="token function">timePad</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span><span class="token method function property-access">getHours</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">':'</span> <span class="token operator">+</span> <span class="token function">timePad</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span><span class="token method function property-access">getMinutes</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>


<span class="token keyword module">export</span> <span class="token keyword">function</span> <span class="token function">formatHMS</span><span class="token punctuation">(</span>d <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword control-flow">return</span> <span class="token function">formatHM</span><span class="token punctuation">(</span>d<span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">':'</span> <span class="token operator">+</span> <span class="token function">timePad</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span><span class="token method function property-access">getSeconds</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code>

The resulting development bundle removes (tree shakes) get() from dom.js but includes all the time.js functions. A source map is also generated:

<code class="javascript language-javascript">
<span class="token keyword">function</span> <span class="token function">getAll</span><span class="token punctuation">(</span><span class="token parameter">selector<span class="token punctuation">,</span> doc <span class="token operator">=</span> <span class="token dom variable">document</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword control-flow">return</span> <span class="token known-class-name class-name">Array</span><span class="token punctuation">.</span><span class="token keyword module">from</span><span class="token punctuation">(</span>doc<span class="token punctuation">.</span><span class="token method function property-access">querySelectorAll</span><span class="token punctuation">(</span>selector<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>


<span class="token keyword">function</span> <span class="token function">timePad</span><span class="token punctuation">(</span><span class="token parameter">n</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword control-flow">return</span> <span class="token known-class-name class-name">String</span><span class="token punctuation">(</span>n<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token method function property-access">padStart</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">,</span> <span class="token string">"0"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">function</span> <span class="token function">formatHM</span><span class="token punctuation">(</span>d <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword control-flow">return</span> <span class="token function">timePad</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span><span class="token method function property-access">getHours</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">":"</span> <span class="token operator">+</span> <span class="token function">timePad</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span><span class="token method function property-access">getMinutes</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">function</span> <span class="token function">formatHMS</span><span class="token punctuation">(</span>d <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword control-flow">return</span> <span class="token function">formatHM</span><span class="token punctuation">(</span>d<span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">":"</span> <span class="token operator">+</span> <span class="token function">timePad</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span><span class="token method function property-access">getSeconds</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>


<span class="token keyword">var</span> clock <span class="token operator">=</span> <span class="token function">getAll</span><span class="token punctuation">(</span><span class="token string">".clock"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword control-flow">if</span> <span class="token punctuation">(</span>clock<span class="token punctuation">.</span><span class="token property-access">length</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">"initializing clock"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token function">setInterval</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token punctuation">{</span>
    clock<span class="token punctuation">.</span><span class="token method function property-access">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">c</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> c<span class="token punctuation">.</span><span class="token property-access">textContent</span> <span class="token operator">=</span> <span class="token function">formatHMS</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">1e3</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

</code>

(Note that esbuild can rewrite let and const to var for correctness and speed.)

The resulting production bundle minifies the code to 322 characters:

<code class="javascript language-javascript"><span class="token keyword">function</span> <span class="token function">o</span><span class="token punctuation">(</span><span class="token parameter">t<span class="token punctuation">,</span>c<span class="token operator">=</span><span class="token dom variable">document</span></span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword control-flow">return</span> <span class="token known-class-name class-name">Array</span><span class="token punctuation">.</span><span class="token keyword module">from</span><span class="token punctuation">(</span>c<span class="token punctuation">.</span><span class="token method function property-access">querySelectorAll</span><span class="token punctuation">(</span>t<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token keyword">function</span> <span class="token function">e</span><span class="token punctuation">(</span><span class="token parameter">t</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword control-flow">return</span> <span class="token known-class-name class-name">String</span><span class="token punctuation">(</span>t<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token method function property-access">padStart</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">,</span><span class="token string">"0"</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token keyword">function</span> <span class="token function">l</span><span class="token punctuation">(</span>t<span class="token operator">=</span><span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword control-flow">return</span> <span class="token function">e</span><span class="token punctuation">(</span>t<span class="token punctuation">.</span><span class="token method function property-access">getHours</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token operator">+</span><span class="token string">":"</span><span class="token operator">+</span><span class="token function">e</span><span class="token punctuation">(</span>t<span class="token punctuation">.</span><span class="token method function property-access">getMinutes</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token keyword">function</span> <span class="token function">r</span><span class="token punctuation">(</span>t<span class="token operator">=</span><span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword control-flow">return</span> <span class="token function">l</span><span class="token punctuation">(</span>t<span class="token punctuation">)</span><span class="token operator">+</span><span class="token string">":"</span><span class="token operator">+</span><span class="token function">e</span><span class="token punctuation">(</span>t<span class="token punctuation">.</span><span class="token method function property-access">getSeconds</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token keyword">var</span> n<span class="token operator">=</span><span class="token function">o</span><span class="token punctuation">(</span><span class="token string">".clock"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>n<span class="token punctuation">.</span><span class="token property-access">length</span><span class="token operator">&&</span><span class="token function">setInterval</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token arrow operator">=></span><span class="token punctuation">{</span>n<span class="token punctuation">.</span><span class="token method function property-access">forEach</span><span class="token punctuation">(</span><span class="token parameter">t</span><span class="token arrow operator">=></span>t<span class="token punctuation">.</span><span class="token property-access">textContent</span><span class="token operator">=</span><span class="token function">r</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token punctuation">,</span><span class="token number">1e3</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code>

CSS Bundling

CSS bundling in the example project uses a similar context object to JavaScript above:

<code class="javascript language-javascript">
<span class="token keyword">const</span> buildCSS <span class="token operator">=</span> <span class="token keyword control-flow">await</span> esbuild<span class="token punctuation">.</span><span class="token method function property-access">context</span><span class="token punctuation">(</span><span class="token punctuation">{</span>

  entryPoints<span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token string">'./src/css/main.css'</span> <span class="token punctuation">]</span><span class="token punctuation">,</span>
  bundle<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
  target<span class="token punctuation">,</span>
  external<span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'/images/*'</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
  loader<span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token string">'.png'</span><span class="token operator">:</span> <span class="token string">'file'</span><span class="token punctuation">,</span>
    <span class="token string">'.jpg'</span><span class="token operator">:</span> <span class="token string">'file'</span><span class="token punctuation">,</span>
    <span class="token string">'.svg'</span><span class="token operator">:</span> <span class="token string">'dataurl'</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  logLevel<span class="token operator">:</span> productionMode <span class="token operator">?</span> <span class="token string">'error'</span> <span class="token operator">:</span> <span class="token string">'info'</span><span class="token punctuation">,</span>
  minify<span class="token operator">:</span> productionMode<span class="token punctuation">,</span>
  sourcemap<span class="token operator">:</span> <span class="token operator">!</span>productionMode <span class="token operator">&&</span> <span class="token string">'linked'</span><span class="token punctuation">,</span>
  outdir<span class="token operator">:</span> <span class="token string">'./build/css'</span>

<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code>

It defines an external option as an array of files and paths to exclude from the build. In the example project, files in the src/images/ directory are copied to the build directory so the HTML, CSS, or JavaScript can reference them directly. If this was not set, esbuild would copy files to the output build/css/ directory when using them in background-image or similar properties.

The loader option changes how esbuild handles an imported file that’s not referenced as an external asset. In this example:

  • SVG images become inlined as data URIs
  • PNG and JPG images are copied to the build/css/ directory and referenced as files

CSS input and output files

The entry src/css/main.css file imports variables.css and elements.css from the partials sub-folder:

<code class="css language-css">
<span class="token atrule"><span class="token rule">@import</span> <span class="token string">'./partials/variables.css'</span><span class="token punctuation">;</span></span>
<span class="token atrule"><span class="token rule">@import</span> <span class="token string">'./partials/elements.css'</span><span class="token punctuation">;</span></span>
</code>

variables.css defines default custom properties:

<code class="css language-css">
<span class="token selector"><span class="token pseudo-class">:root</span></span> <span class="token punctuation">{</span>
  <span class="token variable">--font-body</span><span class="token punctuation">:</span> sans-serif<span class="token punctuation">;</span>
  <span class="token variable">--color-fore</span><span class="token punctuation">:</span> <span class="token hexcode color">#fff</span><span class="token punctuation">;</span>
  <span class="token variable">--color-back</span><span class="token punctuation">:</span> <span class="token hexcode color">#112</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code>

elements.css defines all styles. Note:

  • the body has a background image loaded from the external images directory
  • the h1 is nested inside header
  • the h1 has a background SVG which will be inlined
  • the target browsers require no vendor prefixes
<code class="css language-css">
<span class="token selector">*<span class="token punctuation">,</span> *<span class="token pseudo-element">::before</span><span class="token punctuation">,</span> <span class="token pseudo-element">::after</span></span> <span class="token punctuation">{</span>
  <span class="token property">box-sizing</span><span class="token punctuation">:</span> border-box<span class="token punctuation">;</span>
  <span class="token property">font-weight</span><span class="token punctuation">:</span> normal<span class="token punctuation">;</span>
  <span class="token property">padding</span><span class="token punctuation">:</span> <span class="token number">0</span><span class="token punctuation">;</span>
  <span class="token property">margin</span><span class="token punctuation">:</span> <span class="token number">0</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token selector">body</span> <span class="token punctuation">{</span>
  <span class="token property">font-family</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--font-body</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--color-fore</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--color-back</span><span class="token punctuation">)</span> <span class="token url"><span class="token function">url</span><span class="token punctuation">(</span>/images/web.png<span class="token punctuation">)</span></span> repeat<span class="token punctuation">;</span>
  <span class="token property">margin</span><span class="token punctuation">:</span> <span class="token number">1</span><span class="token unit">em</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>


<span class="token selector">header</span> <span class="token punctuation">{</span>

  <span class="token selector">& h1</span> <span class="token punctuation">{</span>
    <span class="token property">font-size</span><span class="token punctuation">:</span> <span class="token number">2</span><span class="token unit">em</span><span class="token punctuation">;</span>
    <span class="token property">padding-left</span><span class="token punctuation">:</span> <span class="token number">1.5</span><span class="token unit">em</span><span class="token punctuation">;</span>
    <span class="token property">margin</span><span class="token punctuation">:</span> <span class="token number">0.5</span><span class="token unit">em</span> <span class="token number">0</span><span class="token punctuation">;</span>
    <span class="token property">background</span><span class="token punctuation">:</span> <span class="token url"><span class="token function">url</span><span class="token punctuation">(</span>../../icons/clock.svg<span class="token punctuation">)</span></span> no-repeat<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

<span class="token punctuation">}</span>

<span class="token selector"><span class="token class">.clock</span></span> <span class="token punctuation">{</span>
  <span class="token property">display</span><span class="token punctuation">:</span> block<span class="token punctuation">;</span>
  <span class="token property">font-size</span><span class="token punctuation">:</span> <span class="token number">5</span><span class="token unit">em</span><span class="token punctuation">;</span>
  <span class="token property">text-align</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span>
  <span class="token property">font-variant-numeric</span><span class="token punctuation">:</span> tabular-nums<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code>

The resulting development bundle expands the nested syntax, inlines the SVG, and generates a source map:

<code class="css language-css">
<span class="token selector"><span class="token pseudo-class">:root</span></span> <span class="token punctuation">{</span>
  <span class="token variable">--font-body</span><span class="token punctuation">:</span> sans-serif<span class="token punctuation">;</span>
  <span class="token variable">--color-fore</span><span class="token punctuation">:</span> <span class="token hexcode color">#fff</span><span class="token punctuation">;</span>
  <span class="token variable">--color-back</span><span class="token punctuation">:</span> <span class="token hexcode color">#112</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>


<span class="token selector">*<span class="token punctuation">,</span>
*<span class="token pseudo-element">::before</span><span class="token punctuation">,</span>
<span class="token pseudo-element">::after</span></span> <span class="token punctuation">{</span>
  <span class="token property">box-sizing</span><span class="token punctuation">:</span> border-box<span class="token punctuation">;</span>
  <span class="token property">font-weight</span><span class="token punctuation">:</span> normal<span class="token punctuation">;</span>
  <span class="token property">padding</span><span class="token punctuation">:</span> <span class="token number">0</span><span class="token punctuation">;</span>
  <span class="token property">margin</span><span class="token punctuation">:</span> <span class="token number">0</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">body</span> <span class="token punctuation">{</span>
  <span class="token property">font-family</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--font-body</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--color-fore</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--color-back</span><span class="token punctuation">)</span> <span class="token url"><span class="token function">url</span><span class="token punctuation">(</span>/images/web.png<span class="token punctuation">)</span></span> repeat<span class="token punctuation">;</span>
  <span class="token property">margin</span><span class="token punctuation">:</span> <span class="token number">1</span><span class="token unit">em</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">header h1</span> <span class="token punctuation">{</span>
  <span class="token property">font-size</span><span class="token punctuation">:</span> <span class="token number">2</span><span class="token unit">em</span><span class="token punctuation">;</span>
  <span class="token property">padding-left</span><span class="token punctuation">:</span> <span class="token number">1.5</span><span class="token unit">em</span><span class="token punctuation">;</span>
  <span class="token property">margin</span><span class="token punctuation">:</span> <span class="token number">0.5</span><span class="token unit">em</span> <span class="token number">0</span><span class="token punctuation">;</span>
  <span class="token property">background</span><span class="token punctuation">:</span> <span class="token url"><span class="token function">url</span><span class="token punctuation">(</span><span class="token string url">'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><style>*{fill:none;stroke:%23fff;stroke-width:1.5;stroke-miterlimit:10}<\/style></defs><circle cx="12" cy="12" r="10.5"></circle><circle cx="12" cy="12" r="0.95"></circle><polyline points="12 4.36 12 12 16.77 16.77"></polyline></svg>'</span><span class="token punctuation">)</span></span> no-repeat<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector"><span class="token class">.clock</span></span> <span class="token punctuation">{</span>
  <span class="token property">display</span><span class="token punctuation">:</span> block<span class="token punctuation">;</span>
  <span class="token property">font-size</span><span class="token punctuation">:</span> <span class="token number">5</span><span class="token unit">em</span><span class="token punctuation">;</span>
  <span class="token property">text-align</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span>
  <span class="token property">font-variant-numeric</span><span class="token punctuation">:</span> tabular-nums<span class="token punctuation">;</span>
<span class="token punctuation">}</span>



</code>

The resulting production bundle minifies the code to 764 characters (the SVG is omitted here):

<code class="css language-css"><span class="token selector"><span class="token pseudo-class">:root</span></span><span class="token punctuation">{</span><span class="token variable">--font-body</span><span class="token punctuation">:</span> sans-serif<span class="token punctuation">;</span><span class="token variable">--color-fore</span><span class="token punctuation">:</span> <span class="token hexcode color">#fff</span><span class="token punctuation">;</span><span class="token variable">--color-back</span><span class="token punctuation">:</span> <span class="token hexcode color">#112</span><span class="token punctuation">}</span><span class="token selector">*<span class="token punctuation">,</span>*<span class="token pseudo-element">:before</span><span class="token punctuation">,</span><span class="token pseudo-element">:after</span></span><span class="token punctuation">{</span><span class="token property">box-sizing</span><span class="token punctuation">:</span>border-box<span class="token punctuation">;</span><span class="token property">font-weight</span><span class="token punctuation">:</span><span class="token number">400</span><span class="token punctuation">;</span><span class="token property">padding</span><span class="token punctuation">:</span><span class="token number">0</span><span class="token punctuation">;</span><span class="token property">margin</span><span class="token punctuation">:</span><span class="token number">0</span><span class="token punctuation">}</span><span class="token selector">body</span><span class="token punctuation">{</span><span class="token property">font-family</span><span class="token punctuation">:</span><span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--font-body</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token property">color</span><span class="token punctuation">:</span><span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--color-fore</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token property">background</span><span class="token punctuation">:</span><span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--color-back</span><span class="token punctuation">)</span> <span class="token url"><span class="token function">url</span><span class="token punctuation">(</span>/images/web.png<span class="token punctuation">)</span></span> repeat<span class="token punctuation">;</span><span class="token property">margin</span><span class="token punctuation">:</span><span class="token number">1</span><span class="token unit">em</span><span class="token punctuation">}</span><span class="token selector">header h1</span><span class="token punctuation">{</span><span class="token property">font-size</span><span class="token punctuation">:</span><span class="token number">2</span><span class="token unit">em</span><span class="token punctuation">;</span><span class="token property">padding-left</span><span class="token punctuation">:</span><span class="token number">1.5</span><span class="token unit">em</span><span class="token punctuation">;</span><span class="token property">margin</span><span class="token punctuation">:</span><span class="token number">.5</span><span class="token unit">em</span> <span class="token number">0</span><span class="token punctuation">;</span><span class="token property">background</span><span class="token punctuation">:</span><span class="token url"><span class="token function">url</span><span class="token punctuation">(</span><span class="token string url">'data:image/svg+xml,<svg...></svg>'</span><span class="token punctuation">)</span></span> no-repeat<span class="token punctuation">}</span><span class="token selector"><span class="token class">.clock</span></span><span class="token punctuation">{</span><span class="token property">display</span><span class="token punctuation">:</span>block<span class="token punctuation">;</span><span class="token property">font-size</span><span class="token punctuation">:</span><span class="token number">5</span><span class="token unit">em</span><span class="token punctuation">;</span><span class="token property">text-align</span><span class="token punctuation">:</span>center<span class="token punctuation">;</span><span class="token property">font-variant-numeric</span><span class="token punctuation">:</span>tabular-nums<span class="token punctuation">}</span>
</code>

Watching, Rebuilding, and Serving

The remainder of the esbuild.config.js script bundles once for production builds before terminating:

<code class="javascript language-javascript"><span class="token keyword control-flow">if</span> <span class="token punctuation">(</span>productionMode<span class="token punctuation">)</span> <span class="token punctuation">{</span>

  
  <span class="token keyword control-flow">await</span> buildCSS<span class="token punctuation">.</span><span class="token method function property-access">rebuild</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  buildCSS<span class="token punctuation">.</span><span class="token method function property-access">dispose</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token keyword control-flow">await</span> buildJS<span class="token punctuation">.</span><span class="token method function property-access">rebuild</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  buildJS<span class="token punctuation">.</span><span class="token method function property-access">dispose</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token punctuation">}</span>
</code>

During development builds, the script keeps running, watches for file changes, and automatically bundles again. The buildCSS context launches a development web server with build/ as the root directory:

<code class="javascript language-javascript"><span class="token keyword control-flow">else</span> <span class="token punctuation">{</span>

  
  <span class="token keyword control-flow">await</span> buildCSS<span class="token punctuation">.</span><span class="token method function property-access">watch</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token keyword control-flow">await</span> buildJS<span class="token punctuation">.</span><span class="token method function property-access">watch</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  
  <span class="token keyword control-flow">await</span> buildCSS<span class="token punctuation">.</span><span class="token method function property-access">serve</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
    servedir<span class="token operator">:</span> <span class="token string">'./build'</span>
  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token punctuation">}</span>
</code>

Start the development build with:

<code class="bash language-bash"><span class="token function">npm</span> start
</code>

Then navigate to localhost:8000 to view the page.

Unlike Browsersync, you’ll need to add your own code to development pages to live reload. When changes occur, esbuild sends information about the update via a server-sent event. The simplest option is to fully reload the page when any change occurs:

<code class="javascript language-javascript"><span class="token keyword">new</span> <span class="token class-name">EventSource</span><span class="token punctuation">(</span><span class="token string">'/esbuild'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token method function property-access">addEventListener</span><span class="token punctuation">(</span><span class="token string">'change'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token dom variable">location</span><span class="token punctuation">.</span><span class="token method function property-access">reload</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code>

The example project uses the CSS context object to create the server. That’s because I prefer to manually refresh JavaScript changes — and because I couldn’t find a way for esbuild to send an event for both CSS and JS updates! The HTML page includes the following script to replace updated CSS files without a full page refresh (a hot-reload):

<code class="markup language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>module<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
// esbuild server-sent event - live reload CSS
new EventSource('/esbuild').addEventListener('change', e => {

  const { added, removed, updated } = JSON.parse(e.data);

  // reload when CSS files are added or removed
  if (added.length || removed.length) {
    location.reload();
    return;
  }

  // replace updated CSS files
  Array.from(document.getElementsByTagName('link')).forEach(link => {

    const url = new URL(link.href), path = url.pathname;

    if (updated.includes(path) && url.host === location.host) {

      const css = link.cloneNode();
      css.onload = () => link.remove();
      css.href = `${ path }?${ +new Date() }`;
      link.after(css);

    }

  })

});
</code>

Note that esbuild doesn’t currently support JavaScript hot-reloading — not that I’d trust it anyway!

Summary

With a little configuration, esbuild could be enough to handle all your project’s development and production build requirements.

There’s a comprehensive set of plugins should you require more advanced functionality. Be aware these often include Sass, PostCSS, or similar build tools, so they effectively use esbuild as a task runner. You can always create your own plugins if you need more lightweight, custom options.

I’ve been using esbuild for a year. The speed is astounding compared to similar bundlers, and new features appear frequently. The only minor downside is breaking changes that incur maintenance.

esbuild doesn’t claim to be a unified, all-in-one build tool, but it’s probably closer to that goal than Rome.

Source link

Leave a Reply

Your email address will not be published. Required fields are marked *