Setting up esbuild for TypeScript libraries

esbuild is a new build tool for JavaScript that claims to be 10-100x faster than similar projects (webpack, rollup, etc). I’ve started using it for TypeScript development and have been amazed by the performance. TypeScript compilation is instantaneous compared to the native TypeScript compiler. This a huge benefit for my developer productivity!

As well as being extremely fast, esbuild is simple to setup & use, aims to work out of the box and handles all the common build tasks (bundling, minification, generating source maps, etc) needed for most projects. I’ve found it much quicker to learn than previous experiences getting started with tools like Webpack.

In this blog post, I’m going to explain the simple configuration need to build a TypeScript library for the Node.js runtime. The build process is configured using esbuild’s JS API. Here’s the configuration I ended up with. It should be a good starter example for most TypeScript projects.

const esbuild = require('esbuild')

// Automatically exclude all node_modules from the bundled version
const { nodeExternalsPlugin } = require('esbuild-node-externals')

esbuild.build({
  entryPoints: ['./src/index.ts'],
  outfile: 'lib/index.js',
  bundle: true,
  minify: true,
  platform: 'node',
  sourcemap: true,
  target: 'node14',
  plugins: [nodeExternalsPlugin()]
}).catch(() => process.exit(1))

I’ll explain it line by line - but if you just want to get started using it - copy the source code to a JavaScript file (esbuild.config.js) and run node esbuild.config.js. This will compile all the source code referenced from the src/index.ts file into a single lib/index.js file. The compiled code is bundled and minified. It has source maps generated and typescript type definition files.

esbuild configuration script

esbuild can be executed using a CLI tool or through the APIs (JavaScript and Go). esbuild does not support JSON configuration files for build definitions (unlike webpack or rollup). Developers are expected to use the project build APIs directly.

The esbuild library is available an NPM package for Node.js. This template JavaScript code uses that package to stimulate a JSON configuration file for build parameters. Running that script in Node.js invokes the esbuild bundler with config parameters from the JavaScript object (config).

const esbuild = require('esbuild')

const config = {
  // esbuild configuration option - same as the CLI parameters
}

esbuild.build(config).catch(() => process.exit(1))

Running this script file will compile all the source files and exit once finished. The tool does support a watch mode flag to run the build continually.

esbuild configuration parameters

Here is the explanation of the configuration parameters I’ve used.

  • entryPoints: ['./src/index.ts'] This is the entry point for the application. All the separate source files for the library are referenced from this source file. esbuild will recursively bundle all dependencies referenced. TS config files are automatically processed to calculate imports path. If you want to include other source files or directories not referenced from index.ts - include them here.
  • outfile: 'lib/index.js' This is the output file for the compiled JavaScript library. It produces a single bundled & minified file with all the library source code. If you want to produce multiple output files (i.e. non-bundled code) - use outdir instead of multiple output files.
  • bundle: true Bundle all input source files discovered into a single output file - rather than producing an output file per input file. This is disabled by default.
  • minify: true Minification for compiled JavaScript code. Reduces file size for output files by stripping whitespace, shortening syntax and renaming variables. This is disabled by default. The esbuild minifier does not do complex minification, e.g dead-code elimination, but is probably good enough for most cases.
  • platform: 'node' Expected runtime for compiled code. If this value isn’t set - it defaults to ‘browser’. Using node generates CommonJS modules and stops esbuild trying to bundle built-in Node.js libraries, e.g. fs.
  • sourcemap: true Generate source maps for debugging minified code. The source map is available as index.js.map. This source-map-support library (also written by the esbuild author!) helps utilise source maps when running the compiled library code.
  • target: 'node14' Set the target environment for the JavaScript code produced. JavaScript features used in source files that aren’t available in the target environment will be automatically poly-filled. This property can be a version of Node.js (node16), a JavaScript version (es2107) or a browser engine (chrome58). It also supports multiple values (es2020,chrome58,firefox57,safari11,edge16,node12).
  • plugins: [nodeExternalsPlugin()] This community plugin automatically excludes external Node.js project dependencies from being bundled. It uses the dependencies field of the package.json file. External dependencies will be automatically downloaded by the NPM client when someone installs the library and don’t need publishing in our compiled library code.

and if you understand all that - now you can esbuild too 😀!

If you want to understand more, check the project documentation here.

typescript type checking

esbuild has built-in support for TypeScript. It can compile TypeScript to JavaScript but it does not perform type checking. Type checking the source code can be handled by running the normal TS compiler in a special mode. This disables compiling the source files but still performs type checking and emits type declaration files. This process can run in parallel to the esbuild compilation.

Here are the TSConfig properties needed to run the compiler in this special mode.

{
  "compilerOptions": {
    "emitDeclarationOnly": true,
    "declaration": true,    
  }
}

emitDeclarationOnly turns off compilation and declaration ensures the type definition files are generated.