souporserious

Bundling TypeScript with Esbuild for NPM

3 min read

Compared to other build tools, Esbuild is a radically more performant bundler. This post will look at setting up a build script to compile TypeScript projects for NPM.

Follow along with the example repo here.

Getting Started

First, we’ll install the library and set up our build script:

1npm install esbuild typescript --save-dev
package.json
1"scripts": {
2 "build": "node build.js"
3}
build.js
1const { build } = require('esbuild')
2
3build({
4 entryPoints: ['src/index.ts'],
5 outdir: 'dist',
6 bundle: true,
7})

Esbuild will automatically detect that we’re using TypeScript and attempt to load a tsconfig.json file if available. Note that any compiler options set in tsconfig.json will take precedence over build options.

An important option we need to set is the external property. Since we are publishing this library to NPM we’ll want to exclude any of our dependencies from the final bundle. Convinently enough, we can use our package.json file as a source of truth:

1const { build } = require('esbuild')
2const { dependencies, peerDependencies } = require('./package.json')
3
4build({
5 entryPoints: ['src/index.ts'],
6 outdir: 'dist',
7 bundle: true,
8 external: Object.keys(dependencies).concat(Object.keys(peerDependencies)),
9})

Now, if we run our script, we should see our built files in a dist directory:

1npm run build

Additional Formats

When bundling libraries for NPM, it’s good practice to build multiple files for different browser and bundler formats. We can do this easily by pulling out shared options and passing them into any number of different builds we want to provide. By default, format is set to iife, which bundles for a browser environment. Let’s add a format for ESM users:

build.js
1const { build } = require('esbuild')
2const { dependencies, peerDependencies } = require('./package.json')
3
4const shared = {
5 entryPoints: ['src/index.ts'],
6 bundle: true,
7 external: Object.keys(dependencies).concat(Object.keys(peerDependencies)),
8}
9
10build({
11 ...shared,
12 outfile: 'dist/index.js',
13})
14
15build({
16 ...shared,
17 outfile: 'dist/index.esm.js',
18 format: 'esm',
19})

Type Definitions

Since Esbuild can only bundle our code, we still need to generate definition files. This will allow library consumers to utilize the types we’ve written. While it’s relatively simple to emit multiple declaration files using TypeScript directly, the npm-dts library helps us bundle our types into one succinct file:

1npm install npm-dts --save-dev
build.js
1const { Generator } = require('npm-dts')
2
3new Generator({
4 entry: 'src/index.ts',
5 output: 'dist/index.d.ts',
6}).generate()

Publishing to NPM

Make sure the fields in your package.json file are filled out correctly and point to the proper files we’ve created:

package.json
1{
2 "module": "dist/index.esm.js",
3 "main": "dist/index.js",
4 "typings": "dist/index.d.ts"
5}

Now we’re ready to publish our library! 🎉

1npm publish

Conclusion

We looked at how Esbuild provides a fast build system that we can use to compile and bundle TypeScript when developing libraries for NPM. In the future, I imagine we’ll see a type checker written in a performant language like Go or Rust that will drastically speed up our workflows.

Updated:
  • development
  • esbuild
  • react
Previous post
Reading Context in Pragmas
Next post
Better library DX using JSDoc links
© 2022 Travis Arnold