API

The API can be accessed in one of three languages: on the command line, in JavaScript, and in Go. The concepts and parameters are largely identical between the three languages so they will be presented together here instead of having separate documentation for each language. You can switch between languages using the CLI, JS, and Go tabs in the top-right corner of each code example. Some specifics for each language:

Overview

The two most commonly-used esbuild APIs are build and transform. Each is described below at a high level, followed by documentation for each individual API option.

Build

This is the primary interface to esbuild. You typically pass one or more entry point files to process along with various options, and then esbuild writes the results back out to the file system. Here's a simple example that enables bundling with an output directory:

CLI JS Go
esbuild app.ts --bundle --outdir=dist
import * as esbuild from 'esbuild'

let result = await esbuild.build({
  entryPoints: ['app.ts'],
  bundle: true,
  outdir: 'dist',
})
console.log(result)
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.ts"},
    Bundle:      true,
    Outdir:      "dist",
  })
  if len(result.Errors) != 0 {
    os.Exit(1)
  }
}

Advanced use of the build API involves setting up a long-running build context. This context is an explicit object in JS and Go but is implicit with the CLI. All builds done with a given context share the same build options, and subsequent builds are done incrementally (i.e. they reuse some work from previous builds to improve performance). This is useful for development because esbuild can rebuild your app in the background for you while you work.

There are three different incremental build APIs:

CLI JS Go
esbuild app.ts --bundle --outdir=dist --watch
[watch] build finished, watching for changes...
let ctx = await esbuild.context({
  entryPoints: ['app.ts'],
  bundle: true,
  outdir: 'dist',
})

await ctx.watch()
ctx, err := api.Context(api.BuildOptions{
  EntryPoints: []string{"app.ts"},
  Bundle:      true,
  Outdir:      "dist",
})

err2 := ctx.Watch(api.WatchOptions{})
CLI JS Go
esbuild app.ts --bundle --outdir=dist --serve

 > Local:   http://127.0.0.1:8000/
 > Network: http://192.168.0.1:8000/

127.0.0.1:61302 - "GET /" 200 [1ms]
let ctx = await esbuild.context({
  entryPoints: ['app.ts'],
  bundle: true,
  outdir: 'dist',
})

let { host, port } = await ctx.serve()
ctx, err := api.Context(api.BuildOptions{
  EntryPoints: []string{"app.ts"},
  Bundle:      true,
  Outdir:      "dist",
})

server, err2 := ctx.Serve(api.ServeOptions{})
CLI JS Go
# The CLI does not have an API for "rebuild"
let ctx = await esbuild.context({
  entryPoints: ['app.ts'],
  bundle: true,
  outdir: 'dist',
})

for (let i = 0; i < 5; i++) {
  let result = await ctx.rebuild()
}
ctx, err := api.Context(api.BuildOptions{
  EntryPoints: []string{"app.ts"},
  Bundle:      true,
  Outdir:      "dist",
})

for i := 0; i < 5; i++ {
  result := ctx.Rebuild()
}

These three incremental build APIs can be combined. To enable live reloading (automatically reloading the page when you edit and save a file) you'll need to enable watch and serve together on the same context.

When you are done with a context object, you can call dispose() on the context to wait for existing builds to finish, stop watch and/or serve mode, and free up resources.

The build and context APIs both take the following options:

Transform

This is a limited special-case of build that transforms a string of code representing an in-memory file in an isolated environment that's completely disconnected from any other files. Common uses include minifying code and transforming TypeScript into JavaScript. Here's an example:

CLI JS Go
echo 'let x: number = 1' | esbuild --loader=ts
let x = 1;
import * as esbuild from 'esbuild'

let ts = 'let x: number = 1'
let result = await esbuild.transform(ts, {
  loader: 'ts',
})
console.log(result)
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  ts := "let x: number = 1"
  result := api.Transform(ts, api.TransformOptions{
    Loader: api.LoaderTS,
  })

  if len(result.Errors) == 0 {
    fmt.Printf("%s", result.Code)
  }
}

Taking a string instead of a file as input is more ergonomic for certain use cases. File system isolation has certain advantages (e.g. works in the browser, not affected by nearby package.json files) and certain disadvantages (e.g. can't be used with bundling or plugins). If your use case doesn't fit the transform API then you should use the more general build API instead.

The transform API takes the following options:

General options:
Input:

JS-specific details

The JS API for esbuild comes in both asynchronous and synchronous flavors. The asynchronous API is recommended because it works in all environments and it's faster and more powerful. The synchronous API only works in node and can only do certain things, but it's sometimes necessary in certain node-specific situations. In detail:

Async API

Asynchronous API calls return their results using a promise. Note that you'll likely have to use the .mjs file extension in node due to the use of the import and top-level await keywords:

import * as esbuild from 'esbuild'

let result1 = await esbuild.transform(code, options)
let result2 = await esbuild.build(options)

Pros:

Cons:

Sync API

Synchronous API calls return their results inline:

let esbuild = require('esbuild')

let result1 = esbuild.transformSync(code, options)
let result2 = esbuild.buildSync(options)

Pros:

Cons:

In the browser

The esbuild API can also run in the browser using WebAssembly in a Web Worker. To take advantage of this you will need to install the esbuild-wasm package instead of the esbuild package:

npm install esbuild-wasm

The API for the browser is similar to the API for node except that you need to call initialize() first, and you need to pass the URL of the WebAssembly binary. The synchronous versions of the API are also not available. Assuming you are using a bundler, that would look something like this:

import * as esbuild from 'esbuild-wasm'

await esbuild.initialize({
  wasmURL: './node_modules/esbuild-wasm/esbuild.wasm',
})

let result1 = await esbuild.transform(code, options)
let result2 = esbuild.build(options)

If you're already running this code from a worker and don't want initialize to create another worker, you can pass worker: false to it. Then it will create a WebAssembly module in the same thread as the thread that calls initialize.

You can also use esbuild's API as a script tag in a HTML file without needing to use a bundler by loading the lib/browser.min.js file with a <script> tag. In this case the API creates a global called esbuild that holds the API object:

<script src="./node_modules/esbuild-wasm/lib/browser.min.js"></script>
<script>
  esbuild.initialize({
    wasmURL: './node_modules/esbuild-wasm/esbuild.wasm',
  }).then(() => {
    ...
  })
</script>

If you want to use this API with ECMAScript modules, you should import the esm/browser.min.js file instead:

<script type="module">
  import * as esbuild from './node_modules/esbuild-wasm/esm/browser.min.js'

  await esbuild.initialize({
    wasmURL: './node_modules/esbuild-wasm/esbuild.wasm',
  })

  ...
</script>

General options

Bundle

Supported by: Build

To bundle a file means to inline any imported dependencies into the file itself. This process is recursive so dependencies of dependencies (and so on) will also be inlined. By default esbuild will not bundle the input files. Bundling must be explicitly enabled like this:

CLI JS Go
esbuild in.js --bundle
import * as esbuild from 'esbuild'

console.log(await esbuild.build({
  entryPoints: ['in.js'],
  bundle: true,
  outfile: 'out.js',
}))
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"in.js"},
    Bundle:      true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Refer to the getting started guide for an example of bundling with real-world code.

Note that bundling is different than file concatenation. Passing esbuild multiple input files with bundling enabled will create multiple separate bundles instead of joining the input files together. To join a set of files together with esbuild, import them all into a single entry point file and bundle just that one file with esbuild.

Non-analyzable imports

Import paths are currently only bundled if they are a string literal or a glob pattern. Other forms of import paths are not bundled, and are instead preserved verbatim in the generated output. This is because bundling is a compile-time operation and esbuild doesn't support all forms of run-time path resolution. Here are some examples:

// Analyzable imports (will be bundled by esbuild)
import 'pkg';
import('pkg');
require('pkg');
import(`./locale-${foo}.json`);
require(`./locale-${foo}.json`);

// Non-analyzable imports (will not be bundled by esbuild)
import(`pkg/${foo}`);
require(`pkg/${foo}`);
['pkg'].map(require);

The way to work around non-analyzable imports is to mark the package containing this problematic code as external so that it's not included in the bundle. You will then need to ensure that a copy of the external package is available to your bundled code at run-time.

Some bundlers such as Webpack try to support all forms of run-time path resolution by including all potentially-reachable files in the bundle and then emulating a file system at run-time. However, run-time file system emulation is out of scope and will not be implemented in esbuild. If you really need to bundle code that does this, you will likely need to use another bundler instead of esbuild.

Glob-style imports

Import paths that are evaluated at run-time can now be bundled in certain limited situations. The import path expression must be a form of string concatenation and must start with either ./ or ../. Each non-string expression in the string concatenation chain becomes a wildcard in a glob pattern. Some examples:

// These two forms are equivalent
const json1 = require('./data/' + kind + '.json')
const json2 = require(`./data/${kind}.json`)

When you do this, esbuild will search the file system for all files that match the pattern and include all of them in the bundle along with a map that maps the matching import path to the bundled module. The import expression will be replaced with a lookup into that map. An error will be thrown at run-time if the import path is not present in the map. The generated code will look something like this (unimportant parts were omitted for brevity):

// data/bar.json
var require_bar = ...;

// data/foo.json
var require_foo = ...;

// require("./data/**/*.json") in example.js
var globRequire_data_json = __glob({
  "./data/bar.json": () => require_bar(),
  "./data/foo.json": () => require_foo()
});

// example.js
var json1 = globRequire_data_json("./data/" + kind + ".json");
var json2 = globRequire_data_json(`./data/${kind}.json`);

This feature works with require(...) and import(...) because these can all accept run-time expressions. It does not work with import and export statements because these cannot accept run-time expressions. If you want to prevent esbuild from trying to bundle these imports, you should move the string concatenation expression outside of the require(...) or import(...). For example:

// This will be bundled
const json1 = require('./data/' + kind + '.json')

// This will not be bundled
const path = './data/' + kind + '.json'
const json2 = require(path)

Note that using this feature means esbuild will potentially do a lot of file system I/O to find all possible files that might match the pattern. This is by design, and is not a bug. If this is a concern, there are two ways to reduce the amount of file system I/O that esbuild does:

  1. The simplest approach is to put all files that you want to import for a given run-time import expression in a subdirectory and then include the subdirectory in the pattern. This limits esbuild to searching inside that subdirectory since esbuild doesn't consider .. path elements during pattern-matching.

  2. Another approach is to prevent esbuild from searching into any subdirectory at all. The pattern matching algorithm that esbuild uses only allows a wildcard to match something containing a / path separator if that wildcard has a / before it in the pattern. So for example './data/' + x + '.json' will match x with anything in any subdirectory while './data-' + x + '.json' will only match x with anything in the top-level directory (but not in any subdirectory).

Cancel

Supported by: Build

If you are using rebuild to manually invoke incremental builds, you may want to use this cancel API to end the current build early so that you can start a new one. You can do that like this:

CLI JS Go
# The CLI does not have an API for "cancel"
import * as esbuild from 'esbuild'
import process from 'node:process'

let ctx = await esbuild.context({
  entryPoints: ['app.ts'],
  bundle: true,
  outdir: 'www',
  logLevel: 'info',
})

// Whenever we get some data over stdin
process.stdin.on('data', async () => {
  try {
    // Cancel the already-running build
    await ctx.cancel()

    // Then start a new build
    console.log('build:', await ctx.rebuild())
  } catch (err) {
    console.error(err)
  }
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  ctx, err := api.Context(api.BuildOptions{
    EntryPoints: []string{"app.ts"},
    Bundle:      true,
    Outdir:      "www",
    LogLevel:    api.LogLevelInfo,
  })
  if err != nil {
    os.Exit(1)
  }

  // Whenever we get some data over stdin
  buf := make([]byte, 100)
  for {
    if n, err := os.Stdin.Read(buf); err != nil || n == 0 {
      break
    }
    go func() {
      // Cancel the already-running build
      ctx.Cancel()

      // Then start a new build
      result := ctx.Rebuild()
      fmt.Fprintf(os.Stderr, "build: %v\n", result)
    }()
  }
}

Make sure to wait until the cancel operation is done before starting a new build (i.e. await the returned promise when using JavaScript), otherwise the next rebuild will give you the just-canceled build that still hasn't ended yet. Note that plugin on-end callbacks will still be run regardless of whether or not the build was canceled.

Live reload

Supported by: Build

Live reload is an approach to development where you have your browser open and visible at the same time as your code editor. When you edit and save your source code, the browser automatically reloads and the reloaded version of the app contains your changes. This means you can iterate faster because you don't have to manually switch to your browser, reload, and then switch back to your code editor after every change. It's very helpful when changing CSS, for example.

There is no esbuild API for live reloading directly. Instead, you can construct live reloading by combining watch mode (to automatically start a build when you edit and save a file) and serve mode (to serve the latest build, but block until it's done) plus a small bit of client-side JavaScript code that you add to your app only during development.

The first step is to enable watch and serve together:

CLI JS Go
esbuild app.ts --bundle --outdir=www --watch --servedir=www
import * as esbuild from 'esbuild'

let ctx = await esbuild.context({
  entryPoints: ['app.ts'],
  bundle: true,
  outdir: 'www',
})

await ctx.watch()

let { host, port } = await ctx.serve({
  servedir: 'www',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  ctx, err := api.Context(api.BuildOptions{
    EntryPoints: []string{"app.ts"},
    Bundle:      true,
    Outdir:      "www",
  })
  if err != nil {
    os.Exit(1)
  }

  err2 := ctx.Watch(api.WatchOptions{})
  if err2 != nil {
    os.Exit(1)
  }

  result, err3 := ctx.Serve(api.ServeOptions{
    Servedir: "www",
  })
  if err3 != nil {
    os.Exit(1)
  }
}

The second step is to add some code to your JavaScript that subscribes to the /esbuild server-sent event source. When you get the change event, you can reload the page to get the latest version of the app. You can do this in a single line of code:

new EventSource('/esbuild').addEventListener('change', () => location.reload())

That's it! If you load your app in the browser, the page should now automatically reload when you edit and save a file (assuming there are no build errors).

This should only be included during development, and should not be included in production. One way to remove this code in production is to guard it with an if statement such as if (!window.IS_PRODUCTION) and then use define to set window.IS_PRODUCTION to true in production.

Live reload caveats

Implementing live reloading like this has a few known caveats:

Hot-reloading for CSS

The change event also contains additional information to enable more advanced use cases. It currently contains the added, removed, and updated arrays with the paths of the files that have changed since the previous build, which can be described by the following TypeScript interface:

interface ChangeEvent {
  added: string[]
  removed: string[]
  updated: string[]
}

The code sample below enables "hot reloading" for CSS, which is when the CSS is automatically updated in place without reloading the page. If an event arrives that isn't CSS-related, then the whole page will be reloaded as a fallback:

new EventSource('/esbuild').addEventListener('change', e => {
  const { added, removed, updated } = JSON.parse(e.data)

  if (!added.length && !removed.length && updated.length === 1) {
    for (const link of document.getElementsByTagName("link")) {
      const url = new URL(link.href)

      if (url.host === location.host && url.pathname === updated[0]) {
        const next = link.cloneNode()
        next.href = updated[0] + '?' + Math.random().toString(36).slice(2)
        next.onload = () => link.remove()
        link.parentNode.insertBefore(next, link.nextSibling)
        return
      }
    }
  }

  location.reload()
})

Hot-reloading for JavaScript

Hot-reloading for JavaScript is not currently implemented by esbuild. It's possible to transparently implement hot-reloading for CSS because CSS is stateless, but JavaScript is stateful so you cannot transparently implement hot-reloading for JavaScript like you can for CSS.

Some other development servers implement hot-reloading for JavaScript anyway, but it requires additional APIs, sometimes requires framework-specific hacks, and sometimes introduces transient state-related bugs during an editing session. Doing this is outside of esbuild's scope. You are welcome to use other tools instead of esbuild if hot-reloading for JavaScript is one of your requirements.

However, with esbuild's live-reloading you can persist your app's current JavaScript state in sessionStorage to more easily restore your app's JavaScript state after a page reload. If your app loads quickly (which it already should for your users' sake), live-reloading with JavaScript can be almost as fast as hot-reloading with JavaScript would be.

Platform

Supported by: Build and Transform

By default, esbuild's bundler is configured to generate code intended for the browser. If your bundled code is intended to run in node instead, you should set the platform to node:

CLI JS Go
esbuild app.js --bundle --platform=node
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  bundle: true,
  platform: 'node',
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    Platform:    api.PlatformNode,
    Write:       true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

When the platform is set to browser (the default value):

When the platform is set to node:

When the platform is set to neutral:

See also bundling for the browser and bundling for node.

Rebuild

Supported by: Build

You may want to use this API if your use case involves calling esbuild's build API repeatedly with the same options. For example, this is useful if you are implementing your own file watcher service. Rebuilding is more efficient than building again because some of the data from the previous build is cached and can be reused if the original files haven't changed since the previous build. There are currently two forms of caching used by the rebuild API:

Here's how to do a rebuild:

CLI JS Go
# The CLI does not have an API for "rebuild"
import * as esbuild from 'esbuild'

let ctx = await esbuild.context({
  entryPoints: ['app.js'],
  bundle: true,
  outfile: 'out.js',
})

// Call "rebuild" as many times as you want
for (let i = 0; i < 5; i++) {
  let result = await ctx.rebuild()
}

// Call "dispose" when you're done to free up resources
ctx.dispose()
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  ctx, err := api.Context(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    Outfile:     "out.js",
  })
  if err != nil {
    os.Exit(1)
  }

  // Call "Rebuild" as many times as you want
  for i := 0; i < 5; i++ {
    result := ctx.Rebuild()
    if len(result.Errors) > 0 {
      os.Exit(1)
    }
  }

  // Call "Dispose" when you're done to free up resources
  ctx.Dispose()
}

Serve

Supported by: Build

If you want your app to automatically reload as you edit, you should read about live reloading. It combines serve mode with watch mode to listen for changes to the file system.

Serve mode starts a web server that serves your code to your browser on your device. Here's an example that bundles src/app.ts into www/js/app.js and then also serves the www directory over http://localhost:8000/:

CLI JS Go
esbuild src/app.ts --outdir=www/js --bundle --servedir=www
import * as esbuild from 'esbuild'

let ctx = await esbuild.context({
  entryPoints: ['src/app.ts'],
  outdir: 'www/js',
  bundle: true,
})

let { host, port } = await ctx.serve({
  servedir: 'www',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  ctx, err := api.Context(api.BuildOptions{
    EntryPoints: []string{"src/app.ts"},
    Outdir:     "www/js",
    Bundle:      true,
  })
  if err != nil {
    os.Exit(1)
  }

  server, err2 := ctx.Serve(api.ServeOptions{
    Servedir: "www",
  })
  if err2 != nil {
    os.Exit(1)
  }

  // Returning from main() exits immediately in Go.
  // Block forever so we keep serving and don't exit.
  <-make(chan struct{})
}

If you create the file www/index.html with the following contents, the code contained in src/app.ts will load when you navigate to http://localhost:8000/:

<script src="js/app.js"></script>

One benefit of using esbuild's built-in web server instead of another web server is that whenever you reload, the files that esbuild serves are always up to date. That's not necessarily the case with other development setups. One common setup is to run a local file watcher that rebuilds output files whenever their input files change, and then separately to run a local file server to serve those output files. But that means reloading after an edit may reload the old output files if the rebuild hasn't finished yet. With esbuild's web server, each incoming request starts a rebuild if one is not already in progress, and then waits for the current rebuild to complete before serving the file. This means esbuild never serves stale build results.

Note that this web server is intended to only be used in development. Do not use this in production.

Arguments

The arguments to the serve API are as follows:

CLI JS Go
# Enable serve mode
--serve

# Set the port
--serve=9000

# Set the host and port (IPv4)
--serve=127.0.0.1:9000

# Set the host and port (IPv6)
--serve=[::1]:9000

# Set the directory to serve
--servedir=www

# Enable HTTPS
--keyfile=your.key --certfile=your.cert

# Specify a fallback HTML file
--serve-fallback=some-file.html
interface ServeOptions {
  port?: number
  host?: string
  servedir?: string
  keyfile?: string
  certfile?: string
  fallback?: string
  onRequest?: (args: ServeOnRequestArgs) => void
}

interface ServeOnRequestArgs {
  remoteAddress: string
  method: string
  path: string
  status: number
  timeInMS: number
}
type ServeOptions struct {
  Port      uint16
  Host      string
  Servedir  string
  Keyfile   string
  Certfile  string
  Fallback  string
  OnRequest func(ServeOnRequestArgs)
}

type ServeOnRequestArgs struct {
  RemoteAddress string
  Method        string
  Path          string
  Status        int
  TimeInMS      int
}

Return values

CLI JS Go
# The CLI will print the host and port like this:

 > Local: http://127.0.0.1:8000/
interface ServeResult {
  host: string
  port: number
}
type ServeResult struct {
  Host string
  Port uint16
}

Enabling HTTPS

By default, esbuild's web server uses the http:// protocol. However, certain modern web features are unavailable to HTTP websites. If you want to use these features, then you'll need to tell esbuild to use the https:// protocol instead.

To enable HTTPS with esbuild:

  1. Generate a self-signed certificate. There are many ways to do this. Here's one way, assuming you have the openssl command installed:

     openssl req -x509 -newkey rsa:4096 -keyout your.key -out your.cert -days 9999 -nodes -subj /CN=127.0.0.1 
  2. Pass your.key and your.cert to esbuild using the keyfile and certfile serve arguments.

  3. Click past the scary warning in your browser when you load your page (self-signed certificates aren't secure, but that doesn't matter since we're just doing local development).

If you have more complex needs than this, you can still put a proxy in front of esbuild and use that for HTTPS instead. Note that if you see the message Client sent an HTTP request to an HTTPS server when you load your page, then you are using the incorrect protocol. Replace http:// with https:// in your browser's URL bar.

Keep in mind that esbuild's HTTPS support has nothing to do with security. The only reason to enable HTTPS in esbuild is because browsers have made it impossible to do local development with certain modern web features without jumping through these extra hoops. Please do not use esbuild's development server for anything that needs to be secure. It's only intended for local development and no considerations have been made for production environments whatsoever.

Customizing server behavior

It's not possible to hook into esbuild's local server to customize the behavior of the server itself. Instead, behavior should be customized by putting a proxy in front of esbuild.

Here's a simple example of a proxy server to get you started, using node's built-in http module. It adds a custom 404 page instead of esbuild's default 404 page:

import * as esbuild from 'esbuild'
import http from 'node:http'

// Start esbuild's server on a random local port
let ctx = await esbuild.context({
  // ... your build options go here ...
})

// The return value tells us where esbuild's local server is
let { host, port } = await ctx.serve({ servedir: '.' })

// Then start a proxy server on port 3000
http.createServer((req, res) => {
  const options = {
    hostname: host,
    port: port,
    path: req.url,
    method: req.method,
    headers: req.headers,
  }

  // Forward each incoming request to esbuild
  const proxyReq = http.request(options, proxyRes => {
    // If esbuild returns "not found", send a custom 404 page
    if (proxyRes.statusCode === 404) {
      res.writeHead(404, { 'Content-Type': 'text/html' })
      res.end('<h1>A custom 404 page</h1>')
      return
    }

    // Otherwise, forward the response from esbuild to the client
    res.writeHead(proxyRes.statusCode, proxyRes.headers)
    proxyRes.pipe(res, { end: true })
  })

  // Forward the body of the request to esbuild
  req.pipe(proxyReq, { end: true })
}).listen(3000)

This code starts esbuild's server on random local port and then starts a proxy server on port 3000. During development you would load http://localhost:3000 in your browser, which talks to the proxy. This example demonstrates modifying a response after esbuild has handled the request, but you can also modify or replace the request before esbuild has handled it.

You can do many things with a proxy like this including:

You can also use a real proxy such as nginx if you have more advanced needs.

Tsconfig

Supported by: Build

Normally the build API automatically discovers tsconfig.json files and reads their contents during a build. However, you can also configure a custom tsconfig.json file to use instead. This can be useful if you need to do multiple builds of the same code with different settings:

CLI JS Go
esbuild app.ts --bundle --tsconfig=custom-tsconfig.json
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.ts'],
  bundle: true,
  tsconfig: 'custom-tsconfig.json',
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.ts"},
    Bundle:      true,
    Tsconfig:    "custom-tsconfig.json",
    Write:       true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Tsconfig raw

Supported by: Build and Transform

This option can be used to pass your tsconfig.json file to the transform API, which doesn't access the file system. It can also be used to pass the contents of your tsconfig.json file to the build API inline without writing it to a file. Using it looks like this:

CLI JS Go
echo 'class Foo { foo }' | esbuild --loader=ts --tsconfig-raw='{"compilerOptions":{"useDefineForClassFields":false}}'
import * as esbuild from 'esbuild'

let ts = 'class Foo { foo }'
let result = await esbuild.transform(ts, {
  loader: 'ts',
  tsconfigRaw: `{
    "compilerOptions": {
      "useDefineForClassFields": false,
    },
  }`,
})
console.log(result.code)
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  ts := "class Foo { foo }"

  result := api.Transform(ts, api.TransformOptions{
    Loader: api.LoaderTS,
    TsconfigRaw: `{
      "compilerOptions": {
        "useDefineForClassFields": false,
      },
    }`,
  })

  if len(result.Errors) == 0 {
    fmt.Printf("%s", result.Code)
  }
}

Watch

Supported by: Build

Enabling watch mode tells esbuild to listen for changes on the file system and to automatically rebuild whenever a file changes that could invalidate the build. Using it looks like this:

CLI JS Go
esbuild app.js --outfile=out.js --bundle --watch
[watch] build finished, watching for changes...
import * as esbuild from 'esbuild'

let ctx = await esbuild.context({
  entryPoints: ['app.js'],
  outfile: 'out.js',
  bundle: true,
})

await ctx.watch()
console.log('watching...')
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  ctx, err := api.Context(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Outfile:     "out.js",
    Bundle:      true,
    Write:       true,
  })
  if err != nil {
    os.Exit(1)
  }

  err2 := ctx.Watch(api.WatchOptions{})
  if err2 != nil {
    os.Exit(1)
  }
  fmt.Printf("watching...\n")

  // Returning from main() exits immediately in Go.
  // Block forever so we keep watching and don't exit.
  <-make(chan struct{})
}

If you want to stop watch mode at some point in the future, you can call dispose on the context object to terminate the file watcher:

CLI JS Go
# Use Ctrl+C to stop the CLI in watch mode
import * as esbuild from 'esbuild'

let ctx = await esbuild.context({
  entryPoints: ['app.js'],
  outfile: 'out.js',
  bundle: true,
})

await ctx.watch()
console.log('watching...')

await new Promise(r => setTimeout(r, 10 * 1000))
await ctx.dispose()
console.log('stopped watching')
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"
import "os"
import "time"

func main() {
  ctx, err := api.Context(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Outfile:     "out.js",
    Bundle:      true,
    Write:       true,
  })
  if err != nil {
    os.Exit(1)
  }

  err2 := ctx.Watch(api.WatchOptions{})
  if err2 != nil {
    os.Exit(1)
  }
  fmt.Printf("watching...\n")

  time.Sleep(10 * time.Second)
  ctx.Dispose()
  fmt.Printf("stopped watching\n")
}

Watch mode in esbuild is implemented using polling instead of OS-specific file system APIs for portability. The polling system is designed to use relatively little CPU vs. a more traditional polling system that scans the whole directory tree at once. The file system is still scanned regularly but each scan only checks a random subset of your files, which means a change to a file will be picked up soon after the change is made but not necessarily instantly.

With the current heuristics, large projects should be completely scanned around every 2 seconds so in the worst case it could take up to 2 seconds for a change to be noticed. However, after a change has been noticed the change's path goes on a short list of recently changed paths which are checked on every scan, so further changes to recently changed files should be noticed almost instantly.

Note that it is still possible to implement watch mode yourself using esbuild's rebuild API and a file watcher library of your choice if you don't want to use a polling-based approach.

If you are using the CLI, keep in mind that watch mode will be terminated when esbuild's stdin is closed. This prevents esbuild from accidentally outliving the parent process and unexpectedly continuing to consume resources on the system. If you have a use case that requires esbuild to continue to watch forever even when the parent process has finished, you may use --watch=forever instead of --watch.

Input

Entry points

Supported by: Build

This is an array of files that each serve as an input to the bundling algorithm. They are called "entry points" because each one is meant to be the initial script that is evaluated which then loads all other aspects of the code that it represents. Instead of loading many libraries in your page with <script> tags, you would instead use import statements to import them into your entry point (or into another file that is then imported into your entry point).

Simple apps only need one entry point but additional entry points can be useful if there are multiple logically-independent groups of code such as a main thread and a worker thread, or an app with separate relatively unrelated areas such as a landing page, an editor page, and a settings page. Separate entry points helps introduce separation of concerns and helps reduce the amount of unnecessary code that the browser needs to download. If applicable, enabling code splitting can further reduce download sizes when browsing to a second page whose entry point shares some already-downloaded code with a first page that has already been visited.

The simple way to specify entry points is to just pass an array of file paths:

CLI JS Go
esbuild home.ts settings.ts --bundle --outdir=out
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['home.ts', 'settings.ts'],
  bundle: true,
  write: true,
  outdir: 'out',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"home.ts", "settings.ts"},
    Bundle:      true,
    Write:       true,
    Outdir:      "out",
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

This will generate two output files, out/home.js and out/settings.js corresponding to the two entry points home.ts and settings.ts.

For further control over how the paths of the output files are derived from the corresponding input entry points, you should look into these options:

In addition, you can also specify a fully custom output path for each individual entry point using an alternative entry point syntax:

CLI JS Go
esbuild out1=home.ts out2=settings.ts --bundle --outdir=out
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: [
    { out: 'out1', in: 'home.ts'},
    { out: 'out2', in: 'settings.ts'},
  ],
  bundle: true,
  write: true,
  outdir: 'out',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPointsAdvanced: []api.EntryPoint{{
      OutputPath: "out1",
      InputPath:  "home.ts",
    }, {
      OutputPath: "out2",
      InputPath:  "settings.ts",
    }},
    Bundle: true,
    Write:  true,
    Outdir: "out",
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

This will generate two output files, out/out1.js and out/out2.js corresponding to the two entry points home.ts and settings.ts.

Glob-style entry points

If an entry point contains the * character, then it's considered to be a glob pattern. This means esbuild will use that entry point as a pattern to search for files on the file system and will then replace that entry point with any matching files that were found. So for example, an entry point of *.js will cause esbuild to consider all files in the current directory that end in .js to be entry points.

The glob matcher that esbuild implements is intentionally simple, and does not support more advanced features found in certain other glob libraries. Only two kinds of wildcards are supported:

If you are using esbuild via the CLI, keep in mind that if you do not quote arguments that contain shell metacharacters before you pass them to esbuild, your shell will likely expand them before esbuild sees them. So if you run esbuild "*.js" (with quotes) then esbuild will see an entry point of *.js and the glob-style entry point rules described above will apply. But if you run esbuild *.js (without quotes) then esbuild will see whatever your current shell decided to expand *.js into (which may include seeing nothing at all if your shell expanded it into nothing). Using esbuild's built-in glob pattern support can be a convenient way to ensure cross-platform consistency by avoiding shell-specific behavior, but it requires you to quote your arguments correctly so that your shell doesn't interpret them.

Loader

Supported by: Build and Transform

This option changes how a given input file is interpreted. For example, the js loader interprets the file as JavaScript and the css loader interprets the file as CSS. See the content types page for a complete list of all built-in loaders.

Configuring a loader for a given file type lets you load that file type with an import statement or a require call. For example, configuring the .png file extension to use the data URL loader means importing a .png file gives you a data URL containing the contents of that image:

import url from './example.png'
let image = new Image
image.src = url
document.body.appendChild(image)

import svg from './example.svg'
let doc = new DOMParser().parseFromString(svg, 'application/xml')
let node = document.importNode(doc.documentElement, true)
document.body.appendChild(node)

The above code can be bundled using the build API call like this:

CLI JS Go
esbuild app.js --bundle --loader:.png=dataurl --loader:.svg=text
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  bundle: true,
  loader: {
    '.png': 'dataurl',
    '.svg': 'text',
  },
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    Loader: map[string]api.Loader{
      ".png": api.LoaderDataURL,
      ".svg": api.LoaderText,
    },
    Write: true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

This option is specified differently if you are using the build API with input from stdin, since stdin does not have a file extension. Configuring a loader for stdin with the build API looks like this:

CLI JS Go
echo 'import pkg = require("./pkg")' | esbuild --loader=ts --bundle
import * as esbuild from 'esbuild'

await esbuild.build({
  stdin: {
    contents: 'import pkg = require("./pkg")',
    loader: 'ts',
    resolveDir: '.',
  },
  bundle: true,
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    Stdin: &api.StdinOptions{
      Contents:   "import pkg = require('./pkg')",
      Loader:     api.LoaderTS,
      ResolveDir: ".",
    },
    Bundle: true,
  })
  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

The transform API call just takes a single loader since it doesn't involve interacting with the file system, and therefore doesn't deal with file extensions. Configuring a loader (in this case the ts loader) for the transform API looks like this:

CLI JS Go
echo 'let x: number = 1' | esbuild --loader=ts
let x = 1;
import * as esbuild from 'esbuild'

let ts = 'let x: number = 1'
let result = await esbuild.transform(ts, {
  loader: 'ts',
})
console.log(result.code)
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  ts := "let x: number = 1"
  result := api.Transform(ts, api.TransformOptions{
    Loader: api.LoaderTS,
  })
  if len(result.Errors) == 0 {
    fmt.Printf("%s", result.Code)
  }
}

Stdin

Supported by: Build

Normally the build API call takes one or more file names as input. However, this option can be used to run a build without a module existing on the file system at all. It's called "stdin" because it corresponds to piping a file to stdin on the command line.

In addition to specifying the contents of the stdin file, you can optionally also specify the resolve directory (used to determine where relative imports are located), the sourcefile (the file name to use in error messages and source maps), and the loader (which determines how the file contents are interpreted). The CLI doesn't have a way to specify the resolve directory. Instead, it's automatically set to the current working directory.

Here's how to use this feature:

CLI JS Go
echo 'export * from "./another-file"' | esbuild --bundle --sourcefile=imaginary-file.js --loader=ts --format=cjs
import * as esbuild from 'esbuild'

let result = await esbuild.build({
  stdin: {
    contents: `export * from "./another-file"`,

    // These are all optional:
    resolveDir: './src',
    sourcefile: 'imaginary-file.js',
    loader: 'ts',
  },
  format: 'cjs',
  write: false,
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    Stdin: &api.StdinOptions{
      Contents: "export * from './another-file'",

      // These are all optional:
      ResolveDir: "./src",
      Sourcefile: "imaginary-file.js",
      Loader:     api.LoaderTS,
    },
    Format: api.FormatCommonJS,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Output contents

Supported by: Build and Transform

Use this to insert an arbitrary string at the beginning of generated JavaScript and CSS files. This is commonly used to insert comments:

CLI JS Go
esbuild app.js --banner:js=//comment --banner:css=/*comment*/
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  banner: {
    js: '//comment',
    css: '/*comment*/',
  },
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Banner: map[string]string{
      "js":  "//comment",
      "css": "/*comment*/",
    },
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

This is similar to footer which inserts at the end instead of the beginning.

Note that if you are inserting non-comment code into a CSS file, be aware that CSS ignores all @import rules that come after a non-@import rule (other than a @charset rule), so using a banner to inject CSS rules may accidentally disable imports of external stylesheets.

Charset

Supported by: Build and Transform

By default esbuild's output is ASCII-only. Any non-ASCII characters are escaped using backslash escape sequences. One reason is because non-ASCII characters are misinterpreted by the browser by default, which causes confusion. You have to explicitly add <meta charset="utf-8"> to your HTML or serve it with the correct Content-Type header for the browser to not mangle your code. Another reason is that non-ASCII characters can significantly slow down the browser's parser. However, using escape sequences makes the generated output slightly bigger, and also makes it harder to read.

If you would like for esbuild to print the original characters without using escape sequences and you have ensured that the browser will interpret your code as UTF-8, you can disable character escaping by setting the charset:

CLI JS Go
echo 'let π = Math.PI' | esbuild
let \u03C0 = Math.PI;
echo 'let π = Math.PI' | esbuild --charset=utf8
let π = Math.PI;
import * as esbuild from 'esbuild'
let js = 'let π = Math.PI'
(await esbuild.transform(js)).code
'let \\u03C0 = Math.PI;\n'
(await esbuild.transform(js, {
  charset: 'utf8',
})).code
'let π = Math.PI;\n'
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  js := "let π = Math.PI"

  result1 := api.Transform(js, api.TransformOptions{})

  if len(result1.Errors) == 0 {
    fmt.Printf("%s", result1.Code)
  }

  result2 := api.Transform(js, api.TransformOptions{
    Charset: api.CharsetUTF8,
  })

  if len(result2.Errors) == 0 {
    fmt.Printf("%s", result2.Code)
  }
}

Some caveats:

Supported by: Build and Transform

Use this to insert an arbitrary string at the end of generated JavaScript and CSS files. This is commonly used to insert comments:

CLI JS Go
esbuild app.js --footer:js=//comment --footer:css=/*comment*/
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  footer: {
    js: '//comment',
    css: '/*comment*/',
  },
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Footer: map[string]string{
      "js":  "//comment",
      "css": "/*comment*/",
    },
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

This is similar to banner which inserts at the beginning instead of the end.

Format

Supported by: Build and Transform

This sets the output format for the generated JavaScript files. There are currently three possible values that can be configured: iife, cjs, and esm. When no output format is specified, esbuild picks an output format for you if bundling is enabled (as described below), or doesn't do any format conversion if bundling is disabled.

IIFE

The iife format stands for "immediately-invoked function expression" and is intended to be run in the browser. Wrapping your code in a function expression ensures that any variables in your code don't accidentally conflict with variables in the global scope. If your entry point has exports that you want to expose as a global in the browser, you can configure that global's name using the global name setting. The iife format will automatically be enabled when no output format is specified, bundling is enabled, and platform is set to browser (which it is by default). Specifying the iife format looks like this:

CLI JS Go
echo 'alert("test")' | esbuild --format=iife
(() => {
  alert("test");
})();
import * as esbuild from 'esbuild'

let js = 'alert("test")'
let result = await esbuild.transform(js, {
  format: 'iife',
})
console.log(result.code)
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  js := "alert(\"test\")"

  result := api.Transform(js, api.TransformOptions{
    Format: api.FormatIIFE,
  })

  if len(result.Errors) == 0 {
    fmt.Printf("%s", result.Code)
  }
}

CommonJS

The cjs format stands for "CommonJS" and is intended to be run in node. It assumes the environment contains exports, require, and module. Entry points with exports in ECMAScript module syntax will be converted to a module with a getter on exports for each export name. The cjs format will automatically be enabled when no output format is specified, bundling is enabled, and platform is set to node. Specifying the cjs format looks like this:

CLI JS Go
echo 'export default "test"' | esbuild --format=cjs
...
var stdin_exports = {};
__export(stdin_exports, {
  default: () => stdin_default
});
module.exports = __toCommonJS(stdin_exports);
var stdin_default = "test";
import * as esbuild from 'esbuild'

let js = 'export default "test"'
let result = await esbuild.transform(js, {
  format: 'cjs',
})
console.log(result.code)
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  js := "export default 'test'"

  result := api.Transform(js, api.TransformOptions{
    Format: api.FormatCommonJS,
  })

  if len(result.Errors) == 0 {
    fmt.Printf("%s", result.Code)
  }
}

ESM

The esm format stands for "ECMAScript module". It assumes the environment supports import and export syntax. Entry points with exports in CommonJS module syntax will be converted to a single default export of the value of module.exports. The esm format will automatically be enabled when no output format is specified, bundling is enabled, and platform is set to neutral. Specifying the esm format looks like this:

CLI JS Go
echo 'module.exports = "test"' | esbuild --format=esm
...
var require_stdin = __commonJS({
  "<stdin>"(exports, module) {
    module.exports = "test";
  }
});
export default require_stdin();
import * as esbuild from 'esbuild'

let js = 'module.exports = "test"'
let result = await esbuild.transform(js, {
  format: 'esm',
})
console.log(result.code)
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  js := "module.exports = 'test'"

  result := api.Transform(js, api.TransformOptions{
    Format: api.FormatESModule,
  })

  if len(result.Errors) == 0 {
    fmt.Printf("%s", result.Code)
  }
}

The esm format can be used either in the browser or in node, but you have to explicitly load it as a module. This happens automatically if you import it from another module. Otherwise:

Global name

Supported by: Build and Transform

This option only matters when the format setting is iife (which stands for immediately-invoked function expression). It sets the name of the global variable which is used to store the exports from the entry point:

CLI JS Go
echo 'module.exports = "test"' | esbuild --format=iife --global-name=xyz
import * as esbuild from 'esbuild'

let js = 'module.exports = "test"'
let result = await esbuild.transform(js, {
  format: 'iife',
  globalName: 'xyz',
})
console.log(result.code)
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  js := "module.exports = 'test'"

  result := api.Transform(js, api.TransformOptions{
    Format:     api.FormatIIFE,
    GlobalName: "xyz",
  })

  if len(result.Errors) == 0 {
    fmt.Printf("%s", result.Code)
  }
}

Specifying the global name with the iife format will generate code that looks something like this:

var xyz = (() => {
  ...
  var require_stdin = __commonJS((exports, module) => {
    module.exports = "test";
  });
  return require_stdin();
})();

The global name can also be a compound property expression, in which case esbuild will generate a global variable with that property. Existing global variables that conflict will not be overwritten. This can be used to implement "namespacing" where multiple independent scripts add their exports onto the same global object. For example:

CLI JS Go
echo 'module.exports = "test"' | esbuild --format=iife --global-name='example.versions["1.0"]'
import * as esbuild from 'esbuild'

let js = 'module.exports = "test"'
let result = await esbuild.transform(js, {
  format: 'iife',
  globalName: 'example.versions["1.0"]',
})
console.log(result.code)
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  js := "module.exports = 'test'"

  result := api.Transform(js, api.TransformOptions{
    Format:     api.FormatIIFE,
    GlobalName: `example.versions["1.0"]`,
  })

  if len(result.Errors) == 0 {
    fmt.Printf("%s", result.Code)
  }
}

The compound global name used above generates code that looks like this:

var example = example || {};
example.versions = example.versions || {};
example.versions["1.0"] = (() => {
  ...
  var require_stdin = __commonJS((exports, module) => {
    module.exports = "test";
  });
  return require_stdin();
})();

Supported by: Build and Transform

A "legal comment" is considered to be any statement-level comment in JS or rule-level comment in CSS that contains @license or @preserve or that starts with //! or /*!. These comments are preserved in output files by default since that follows the intent of the original authors of the code. However, this behavior can be configured by using one of the following options:

The default behavior is eof when bundling is enabled and inline otherwise. Setting the legal comment mode looks like this:

CLI JS Go
esbuild app.js --legal-comments=eof
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  legalComments: 'eof',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints:   []string{"app.js"},
    LegalComments: api.LegalCommentsEndOfFile,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Note that "statement-level" for JS and "rule-level" for CSS means the comment must appear in a context where multiple statements or rules are allowed such as in the top-level scope or in a statement or rule block. So comments inside expressions or at the declaration level are not considered legal comments.

Line limit

Supported by: Build and Transform

This setting is a way to prevent esbuild from generating output files with really long lines, which can help editing performance in poorly-implemented text editors. Set this to a positive integer to tell esbuild to end a given line soon after it passes that number of bytes. For example, this wraps long lines soon after they pass ~80 characters:

CLI JS Go
esbuild app.ts --line-limit=80
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.ts'],
  lineLimit: 80,
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.ts"},
    LineLimit:   80,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Lines are truncated after they pass the limit instead of before because it's simpler to check when the limit is passed then to predict when the limit is about to be passed, and because it's faster to avoid backing up and rewriting things when generating an output file. So the limit is only approximate.

This setting applies to both JavaScript and CSS, and works even when minification is disabled. Note that turning this setting on will make your files bigger, as the extra newlines take up additional space in the file (even after gzip compression).

Splitting

Supported by: Build

Code splitting is still a work in progress. It currently only works with the esm output format. There is also a known ordering issue with import statements across code splitting chunks. You can follow the tracking issue for updates about this feature.

This enables "code splitting" which serves two purposes:

When you enable code splitting you must also configure the output directory using the outdir setting:

CLI JS Go
esbuild home.ts about.ts --bundle --splitting --outdir=out --format=esm
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['home.ts', 'about.ts'],
  bundle: true,
  splitting: true,
  outdir: 'out',
  format: 'esm',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"home.ts", "about.ts"},
    Bundle:      true,
    Splitting:   true,
    Outdir:      "out",
    Format:      api.FormatESModule,
    Write:       true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Output location

Allow overwrite

Supported by: Build

Enabling this setting allows output files to overwrite input files. It's not enabled by default because doing so means overwriting your source code, which can lead to data loss if your code is not checked in. But supporting this makes certain workflows easier by avoiding the need for a temporary directory. So you can enable this when you want to deliberately overwrite your source code:

CLI JS Go
esbuild app.js --outdir=. --allow-overwrite
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  outdir: '.',
  allowOverwrite: true,
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints:    []string{"app.js"},
    Outdir:         ".",
    AllowOverwrite: true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Asset names

Supported by: Build

This option controls the file names of the additional output files generated when the loader is set to file. It configures the output paths using a template with placeholders that will be substituted with values specific to the file when the output path is generated. For example, specifying an asset name template of assets/[name]-[hash] puts all assets into a subdirectory called assets inside of the output directory and includes the content hash of the asset in the file name. Doing that looks like this:

CLI JS Go
esbuild app.js --asset-names=assets/[name]-[hash] --loader:.png=file --bundle --outdir=out
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  assetNames: 'assets/[name]-[hash]',
  loader: { '.png': 'file' },
  bundle: true,
  outdir: 'out',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    AssetNames:  "assets/[name]-[hash]",
    Loader: map[string]api.Loader{
      ".png": api.LoaderFile,
    },
    Bundle: true,
    Outdir: "out",
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

There are four placeholders that can be used in asset path templates:

Asset path templates do not need to include a file extension. The original file extension of the asset will be automatically added to the end of the output path after template substitution.

This option is similar to the chunk names and entry names options.

Chunk names

Supported by: Build

This option controls the file names of the chunks of shared code that are automatically generated when code splitting is enabled. It configures the output paths using a template with placeholders that will be substituted with values specific to the chunk when the output path is generated. For example, specifying a chunk name template of chunks/[name]-[hash] puts all generated chunks into a subdirectory called chunks inside of the output directory and includes the content hash of the chunk in the file name. Doing that looks like this:

CLI JS Go
esbuild app.js --chunk-names=chunks/[name]-[hash] --bundle --outdir=out --splitting --format=esm
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  chunkNames: 'chunks/[name]-[hash]',
  bundle: true,
  outdir: 'out',
  splitting: true,
  format: 'esm',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    ChunkNames:  "chunks/[name]-[hash]",
    Bundle:      true,
    Outdir:      "out",
    Splitting:   true,
    Format:      api.FormatESModule,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

There are three placeholders that can be used in chunk path templates:

Chunk path templates do not need to include a file extension. The configured out extension for the appropriate content type will be automatically added to the end of the output path after template substitution.

Note that this option only controls the names for automatically-generated chunks of shared code. It does not control the names for output files related to entry points. The names of these are currently determined from the path of the original entry point file relative to the outbase directory, and this behavior cannot be changed. An additional API option will be added in the future to let you change the file names of entry point output files.

This option is similar to the asset names and entry names options.

Entry names

Supported by: Build

This option controls the file names of the output files corresponding to each input entry point file. It configures the output paths using a template with placeholders that will be substituted with values specific to the file when the output path is generated. For example, specifying an entry name template of [dir]/[name]-[hash] includes a hash of the output file in the file name and puts the files into the output directory, potentially under a subdirectory (see the details about [dir] below). Doing that looks like this:

CLI JS Go
esbuild src/main-app/app.js --entry-names=[dir]/[name]-[hash] --outbase=src --bundle --outdir=out
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['src/main-app/app.js'],
  entryNames: '[dir]/[name]-[hash]',
  outbase: 'src',
  bundle: true,
  outdir: 'out',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"src/main-app/app.js"},
    EntryNames:  "[dir]/[name]-[hash]",
    Outbase:     "src",
    Bundle:      true,
    Outdir:      "out",
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

There are four placeholders that can be used in entry path templates:

Entry path templates do not need to include a file extension. The appropriate out extension based on the file type will be automatically added to the end of the output path after template substitution.

This option is similar to the asset names and chunk names options.

Out extension

Supported by: Build

This option lets you customize the file extension of the files that esbuild generates to something other than .js or .css. In particular, the .mjs and .cjs file extensions have special meaning in node (they indicate a file in ESM and CommonJS format, respectively). This option is useful if you are using esbuild to generate multiple files and you have to use the outdir option instead of the outfile option. You can use it like this:

CLI JS Go
esbuild app.js --bundle --outdir=dist --out-extension:.js=.mjs
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  bundle: true,
  outdir: 'dist',
  outExtension: { '.js': '.mjs' },
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    Outdir:      "dist",
    OutExtension: map[string]string{
      ".js": ".mjs",
    },
    Write: true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Outbase

Supported by: Build

If your build contains multiple entry points in separate directories, the directory structure will be replicated into the output directory relative to the outbase directory. For example, if there are two entry points src/pages/home/index.ts and src/pages/about/index.ts and the outbase directory is src, the output directory will contain pages/home/index.js and pages/about/index.js. Here's how to use it:

CLI JS Go
esbuild src/pages/home/index.ts src/pages/about/index.ts --bundle --outdir=out --outbase=src
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: [
    'src/pages/home/index.ts',
    'src/pages/about/index.ts',
  ],
  bundle: true,
  outdir: 'out',
  outbase: 'src',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{
      "src/pages/home/index.ts",
      "src/pages/about/index.ts",
    },
    Bundle:  true,
    Outdir:  "out",
    Outbase: "src",
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

If the outbase directory isn't specified, it defaults to the lowest common ancestor directory among all input entry point paths. This is src/pages in the example above, which means by default the output directory will contain home/index.js and about/index.js instead.

Outdir

Supported by: Build

This option sets the output directory for the build operation. For example, this command will generate a directory called out:

CLI JS Go
esbuild app.js --bundle --outdir=out
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  bundle: true,
  outdir: 'out',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    Outdir:      "out",
    Write:       true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

The output directory will be generated if it does not already exist, but it will not be cleared if it already contains some files. Any generated files will silently overwrite existing files with the same name. You should clear the output directory yourself before running esbuild if you want the output directory to only contain files from the current run of esbuild.

If your build contains multiple entry points in separate directories, the directory structure will be replicated into the output directory starting from the lowest common ancestor directory among all input entry point paths. For example, if there are two entry points src/home/index.ts and src/about/index.ts, the output directory will contain home/index.js and about/index.js. If you want to customize this behavior, you should change the outbase directory.

Outfile

Supported by: Build

This option sets the output file name for the build operation. This is only applicable if there is a single entry point. If there are multiple entry points, you must use the outdir option instead to specify an output directory. Using outfile looks like this:

CLI JS Go
esbuild app.js --bundle --outfile=out.js
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  bundle: true,
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    Outfile:     "out.js",
    Write:       true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Public path

Supported by: Build

This is useful in combination with the external file loader. By default that loader exports the name of the imported file as a string using the default export. The public path option lets you prepend a base path to the exported string of each file loaded by this loader:

CLI JS Go
esbuild app.js --bundle --loader:.png=file --public-path=https://www.example.com/v1 --outdir=out
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  bundle: true,
  loader: { '.png': 'file' },
  publicPath: 'https://www.example.com/v1',
  outdir: 'out',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    Loader: map[string]api.Loader{
      ".png": api.LoaderFile,
    },
    Outdir:     "out",
    PublicPath: "https://www.example.com/v1",
    Write:      true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Write

Supported by: Build

The build API call can either write to the file system directly or return the files that would have been written as in-memory buffers. By default the CLI and JavaScript APIs write to the file system and the Go API doesn't. To use the in-memory buffers:

JS Go
import * as esbuild from 'esbuild'

let result = await esbuild.build({
  entryPoints: ['app.js'],
  sourcemap: 'external',
  write: false,
  outdir: 'out',
})

for (let out of result.outputFiles) {
  console.log(out.path, out.contents, out.hash, out.text)
}
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Sourcemap:   api.SourceMapExternal,
    Write:       false,
    Outdir:      "out",
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }

  for _, out := range result.OutputFiles {
    fmt.Printf("%v %v %s\n", out.Path, out.Contents, out.Hash)
  }
}

The hash property is a hash of the contents field and has been provided for convenience. The hash algorithm (currently XXH64) is implementation-dependent and may be changed at any time in between esbuild versions.

Path resolution

Alias

Supported by: Build

This feature lets you substitute one package for another when bundling. The example below substitutes the package oldpkg with the package newpkg:

CLI JS Go
esbuild app.js --bundle --alias:oldpkg=newpkg
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  bundle: true,
  write: true,
  alias: {
    'oldpkg': 'newpkg',
  },
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    Write:       true,
    Alias: map[string]string{
      "oldpkg": "newpkg",
    },
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

These new substitutions happen first before all of esbuild's other path resolution logic. One use case for this feature is replacing a node-only package with a browser-friendly package in third-party code that you don't control.

Note that when an import path is substituted using an alias, the resulting import path is resolved in the working directory instead of in the directory containing the source file with the import path. If needed, the working directory that esbuild uses can be set with the working directory feature.

Conditions

Supported by: Build

This feature controls how the exports field in package.json is interpreted. Custom conditions can be added using the conditions setting. You can specify as many of these as you want and the meaning of these is entirely up to package authors. Node has currently only endorsed the development and production custom conditions for recommended use. Here is an example of adding the custom conditions custom1 and custom2:

CLI JS Go
esbuild src/app.js --bundle --conditions=custom1,custom2
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['src/app.js'],
  bundle: true,
  conditions: ['custom1', 'custom2'],
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"src/app.js"},
    Bundle:      true,
    Conditions:  []string{"custom1", "custom2"},
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

How conditions work

Conditions allow you to redirect the same import path to different file locations in different situations. The redirect map containing the conditions and paths is stored in the exports field in the package's package.json file. For example, this would remap require('pkg/foo') to pkg/required.cjs and import 'pkg/foo' to pkg/imported.mjs using the import and require conditions:

{
  "name": "pkg",
  "exports": {
    "./foo": {
      "import": "./imported.mjs",
      "require": "./required.cjs",
      "default": "./fallback.js"
    }
  }
}

Conditions are checked in the order that they appear within the JSON file. So the example above behaves sort of like this:

if (importPath === './foo') {
  if (conditions.has('import')) return './imported.mjs'
  if (conditions.has('require')) return './required.cjs'
  return './fallback.js'
}

By default there are five conditions with special behavior that are built in to esbuild, and cannot be disabled:

The following condition is also automatically included when the platform is set to either browser or node and no custom conditions are configured. If there are any custom conditions configured (even an empty list) then this condition will no longer be automatically included:

Note that when you use the require and import conditions, your package may end up in the bundle multiple times! This is a subtle issue that can cause bugs due to duplicate copies of your code's state in addition to bloating the resulting bundle. This is commonly known as the dual package hazard.

One way of avoiding the dual package hazard that works both for bundlers and when running natively in node is to put all of your code in the require condition as CommonJS and have the import condition just be a light ESM wrapper that calls require on your package and re-exports the package using ESM syntax. This approach doesn't provide good tree-shaking, however, as esbuild doesn't tree-shake CommonJS modules.

Another way of avoiding a dual package hazard is to use the bundler-specific module condition to direct bundlers to always load the ESM version of your package while letting node always fall back to the CommonJS version of your package. Both import and module are intended to be used with ESM but unlike import, the module condition is always active even if the import path was loaded using a require call. This works well with bundlers because bundlers support loading ESM using require, but it's not something that can work with node because node deliberately doesn't implement loading ESM using require.

External

Supported by: Build

You can mark a file or a package as external to exclude it from your build. Instead of being bundled, the import will be preserved (using require for the iife and cjs formats and using import for the esm format) and will be evaluated at run time instead.

This has several uses. First of all, it can be used to trim unnecessary code from your bundle for a code path that you know will never be executed. For example, a package may contain code that only runs in node but you will only be using that package in the browser. It can also be used to import code in node at run time from a package that cannot be bundled. For example, the fsevents package contains a native extension, which esbuild doesn't support. Marking something as external looks like this:

CLI JS Go
echo 'require("fsevents")' > app.js
esbuild app.js --bundle --external:fsevents --platform=node
// app.js
require("fsevents");
import * as esbuild from 'esbuild'
import fs from 'node:fs'

fs.writeFileSync('app.js', 'require("fsevents")')

await esbuild.build({
  entryPoints: ['app.js'],
  outfile: 'out.js',
  bundle: true,
  platform: 'node',
  external: ['fsevents'],
})
package main

import "io/ioutil"
import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  ioutil.WriteFile("app.js", []byte("require(\"fsevents\")"), 0644)

  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Outfile:     "out.js",
    Bundle:      true,
    Write:       true,
    Platform:    api.PlatformNode,
    External:    []string{"fsevents"},
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

You can also use the * wildcard character in an external path to mark all files matching that pattern as external. For example, you can use *.png to remove all .png files or /images/* to remove all paths starting with /images/:

CLI JS Go
esbuild app.js --bundle "--external:*.png" "--external:/images/*"
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  outfile: 'out.js',
  bundle: true,
  external: ['*.png', '/images/*'],
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Outfile:     "out.js",
    Bundle:      true,
    Write:       true,
    External:    []string{"*.png", "/images/*"},
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

External paths are applied both before and after path resolution, which lets you match against both the import path in the source code and the absolute file system path. The path is considered to be external if the external path matches in either case. The specific behavior is as follows:

Main fields

Supported by: Build

When you import a package in node, the main field in that package's package.json file determines which file is imported (along with a lot of other rules). Major JavaScript bundlers including esbuild let you specify additional package.json fields to try when resolving a package. There are at least three such fields commonly in use:

The default main fields depend on the current platform setting. These defaults should be the most widely compatible with the existing package ecosystem. But you can customize them like this if you want to:

CLI JS Go
esbuild app.js --bundle --main-fields=module,main
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  bundle: true,
  mainFields: ['module', 'main'],
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    MainFields:  []string{"module", "main"},
    Write:       true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

For package authors

If you want to author a package that uses the browser field in combination with the module field, then you'll probably want to fill out all four entries in the full CommonJS-vs-ESM and browser-vs-node compatibility matrix. For that you'll need to use the expanded form of the browser field that is a map instead of just a string:

{
  "main": "./node-cjs.js",
  "module": "./node-esm.js",
  "browser": {
    "./node-cjs.js": "./browser-cjs.js",
    "./node-esm.js": "./browser-esm.js"
  }
}

The main field is expected to be CommonJS while the module field is expected to be ESM. The decision about which module format to use is independent from the decision about whether to use a browser-specific or node-specific variant. If you omit one of these four entries, then you risk the wrong variant being chosen. For example, if you omit the entry for the CommonJS browser build, then the CommonJS node build could be chosen instead.

Note that using main, module, and browser is the old way of doing this. There is also a newer way to do this that you may prefer to use instead: the exports field in package.json. It provides a different set of trade-offs. For example, it gives you more precise control over imports for all sub-paths in your package (while main fields only give you control over the entry point), but it may cause your package to be imported multiple times depending on how you configure it.

Node paths

Supported by: Build

Node's module resolution algorithm supports an environment variable called NODE_PATH that contains a list of global directories to use when resolving import paths. These paths are searched for packages in addition to the node_modules directories in all parent directories. You can pass this list of directories to esbuild using an environment variable with the CLI and using an array with the JS and Go APIs:

CLI JS Go
NODE_PATH=someDir esbuild app.js --bundle --outfile=out.js
import * as esbuild from 'esbuild'

await esbuild.build({
  nodePaths: ['someDir'],
  entryPoints: ['app.js'],
  bundle: true,
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    NodePaths:   []string{"someDir"},
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    Outfile:     "out.js",
    Write:       true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

If you are using the CLI and want to pass multiple directories using NODE_PATH, you will have to separate them with : on Unix and ; on Windows. This is the same format that Node itself uses.

Packages

Supported by: Build

Use this setting to control whether all of your package's dependencies are excluded from the bundle or not. This is useful when bundling for node because many npm packages use node-specific features that esbuild doesn't support while bundling (such as __dirname, import.meta.url, fs.readFileSync, and *.node native binary modules). There are two possible values:

Using it looks like this:

CLI JS Go
esbuild app.js --bundle --packages=external
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  bundle: true,
  packages: 'external',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    Packages:    api.PackagesExternal,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Note that this setting only has an effect when bundling is enabled. Also note that marking an import path as external happens after the import path is rewritten by any configured aliases, so the alias feature still has an effect when this setting is used.

Supported by: Build

This setting mirrors the --preserve-symlinks setting in node. If you use that setting (or the similar resolve.symlinks setting in Webpack), you will likely need to enable this setting in esbuild too. It can be enabled like this:

CLI JS Go
esbuild app.js --bundle --preserve-symlinks --outfile=out.js
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  bundle: true,
  preserveSymlinks: true,
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints:      []string{"app.js"},
    Bundle:           true,
    PreserveSymlinks: true,
    Outfile:          "out.js",
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Enabling this setting causes esbuild to determine file identity by the original file path (i.e. the path without following symlinks) instead of the real file path (i.e. the path after following symlinks). This can be beneficial with certain directory structures. Keep in mind that this means a file may be given multiple identities if there are multiple symlinks pointing to it, which can result in it appearing multiple times in generated output files.

Note: The term "symlink" means symbolic link and refers to a file system feature where a path can redirect to another path.

Resolve extensions

Supported by: Build

The resolution algorithm used by node supports implicit file extensions. You can require('./file') and it will check for ./file, ./file.js, ./file.json, and ./file.node in that order. Modern bundlers including esbuild extend this concept to other file types as well. The full order of implicit file extensions in esbuild can be customized using the resolve extensions setting, which defaults to .tsx,.ts,.jsx,.js,.css,.json:

CLI JS Go
esbuild app.js --bundle --resolve-extensions=.ts,.js
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  bundle: true,
  resolveExtensions: ['.ts', '.js'],
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints:       []string{"app.js"},
    Bundle:            true,
    ResolveExtensions: []string{".ts", ".js"},
    Write:             true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Note that esbuild deliberately does not include the new .mjs and .cjs extensions in this list. Node's resolution algorithm doesn't treat these as implicit file extensions, so esbuild doesn't either. If you want to import files with these extensions you should either explicitly add the extensions in your import paths or change this setting to include the additional extensions that you want to be implicit.

Working directory

Supported by: Build

This API option lets you specify the working directory to use for the build. It normally defaults to the current working directory of the process you are using to call esbuild's API. The working directory is used by esbuild for a few different things including resolving relative paths given as API options to absolute paths and pretty-printing absolute paths as relative paths in log messages. Here is how to customize esbuild's working directory:

CLI JS Go
cd "/var/tmp/custom/working/directory"
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['file.js'],
  absWorkingDir: '/var/tmp/custom/working/directory',
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints:   []string{"file.js"},
    AbsWorkingDir: "/var/tmp/custom/working/directory",
    Outfile:       "out.js",
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Note: If you are using Yarn Plug'n'Play, keep in mind that this working directory is used to search for Yarn's manifest file. If you are running esbuild from an unrelated directory, you will have to set this working directory to the directory containing the manifest file (or one of its child directories) for the manifest file to be found by esbuild.

Transformation

JSX

Supported by: Build and Transform

This option tells esbuild what to do about JSX syntax. Here are the available options:

Here's an example of setting the JSX transform to preserve:

CLI JS Go
echo '<div/>' | esbuild --jsx=preserve --loader=jsx
<div />;
import * as esbuild from 'esbuild'

let result = await esbuild.transform('<div/>', {
  jsx: 'preserve',
  loader: 'jsx',
})

console.log(result.code)
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  result := api.Transform("<div/>", api.TransformOptions{
    JSX:    api.JSXPreserve,
    Loader: api.LoaderJSX,
  })

  if len(result.Errors) == 0 {
    fmt.Printf("%s", result.Code)
  }
}

JSX dev

Supported by: Build and Transform

If the JSX transform has been set to automatic, then enabling this setting causes esbuild to automatically inject the file name and source location into each JSX element. Your JSX library can then use this information to help with debugging. If the JSX transform has been set to something other than automatic, then this setting does nothing. Here's an example of enabling this setting:

CLI JS Go
echo '<a/>' | esbuild --loader=jsx --jsx=automatic
import { jsx } from "react/jsx-runtime";
/* @__PURE__ */ jsx("a", {});
echo '<a/>' | esbuild --loader=jsx --jsx=automatic --jsx-dev
import { jsxDEV } from "react/jsx-dev-runtime";
/* @__PURE__ */ jsxDEV("a", {}, void 0, false, {
  fileName: "<stdin>",
  lineNumber: 1,
  columnNumber: 1
}, this);
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.jsx'],
  jsxDev: true,
  jsx: 'automatic',
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.jsx"},
    JSXDev:      true,
    JSX:         api.JSXAutomatic,
    Outfile:     "out.js",
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

JSX factory

Supported by: Build and Transform

This sets the function that is called for each JSX element. Normally a JSX expression such as this:

<div>Example text</div>

is compiled into a function call to React.createElement like this:

React.createElement("div", null, "Example text");

You can call something other than React.createElement by changing the JSX factory. For example, to call the function h instead (which is used by other libraries such as Preact):

CLI JS Go
echo '<div/>' | esbuild --jsx-factory=h --loader=jsx
/* @__PURE__ */ h("div", null);
import * as esbuild from 'esbuild'

let result = await esbuild.transform('<div/>', {
  jsxFactory: 'h',
  loader: 'jsx',
})

console.log(result.code)
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  result := api.Transform("<div/>", api.TransformOptions{
    JSXFactory: "h",
    Loader:     api.LoaderJSX,
  })

  if len(result.Errors) == 0 {
    fmt.Printf("%s", result.Code)
  }
}

Alternatively, if you are using TypeScript, you can just configure JSX for TypeScript by adding this to your tsconfig.json file and esbuild should pick it up automatically without needing to be configured:

{
  "compilerOptions": {
    "jsxFactory": "h"
  }
}

If you want to configure this on a per-file basis, you can do that by using a // @jsx h comment. Note that this setting does not apply when the JSX transform has been set to automatic.

JSX fragment

Supported by: Build and Transform

This sets the function that is called for each JSX fragment. Normally a JSX fragment expression such as this:

<>Stuff</>

is compiled into a use of the React.Fragment component like this:

React.createElement(React.Fragment, null, "Stuff");

You can use a component other than React.Fragment by changing the JSX fragment. For example, to use the component Fragment instead (which is used by other libraries such as Preact):

CLI JS Go
echo '<>x</>' | esbuild --jsx-fragment=Fragment --loader=jsx
/* @__PURE__ */ React.createElement(Fragment, null, "x");
import * as esbuild from 'esbuild'

let result = await esbuild.transform('<>x</>', {
  jsxFragment: 'Fragment',
  loader: 'jsx',
})

console.log(result.code)
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  result := api.Transform("<>x</>", api.TransformOptions{
    JSXFragment: "Fragment",
    Loader:      api.LoaderJSX,
  })

  if len(result.Errors) == 0 {
    fmt.Printf("%s", result.Code)
  }
}

Alternatively, if you are using TypeScript, you can just configure JSX for TypeScript by adding this to your tsconfig.json file and esbuild should pick it up automatically without needing to be configured:

{
  "compilerOptions": {
    "jsxFragmentFactory": "Fragment"
  }
}

If you want to configure this on a per-file basis, you can do that by using a // @jsxFrag Fragment comment. Note that this setting does not apply when the JSX transform has been set to automatic.

JSX import source

Supported by: Build and Transform

If the JSX transform has been set to automatic, then setting this lets you change which library esbuild uses to automatically import its JSX helper functions from. Note that this only works with the JSX transform that's specific to React 17+. If you set the JSX import source to your-pkg, then that package must expose at least the following exports:

import { createElement } from "your-pkg"
import { Fragment, jsx, jsxs } from "your-pkg/jsx-runtime"
import { Fragment, jsxDEV } from "your-pkg/jsx-dev-runtime"

The /jsx-runtime and /jsx-dev-runtime subpaths are hard-coded by design and cannot be changed. The jsx and jsxs imports are used when JSX dev mode is off and the jsxDEV import is used when JSX dev mode is on. The meaning of these is described in React's documentation about their new JSX transform. The createElement import is used regardless of the JSX dev mode when an element has a prop spread followed by a key prop, which looks like this:

return <div {...props} key={key} />

Here's an example of setting the JSX import source to preact:

CLI JS Go
esbuild app.jsx --jsx-import-source=preact --jsx=automatic
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.jsx'],
  jsxImportSource: 'preact',
  jsx: 'automatic',
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints:     []string{"app.jsx"},
    JSXImportSource: "preact",
    JSX:             api.JSXAutomatic,
    Outfile:         "out.js",
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Alternatively, if you are using TypeScript, you can just configure the JSX import source for TypeScript by adding this to your tsconfig.json file and esbuild should pick it up automatically without needing to be configured:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  }
}

And if you want to control this setting on the per-file basis, you can do that with a // @jsxImportSource your-pkg comment in each file. You may also need to add a // @jsxRuntime automatic comment as well if the JSX transform has not already been set by other means, or if you want that to be set on a per-file basis as well.

JSX side effects

Supported by: Build and Transform

By default esbuild assumes that JSX expressions are side-effect free, which means they are annoated with /* @__PURE__ */ comments and are removed during bundling when they are unused. This follows the common use of JSX for virtual DOM and applies to the vast majority of JSX libraries. However, some people have written JSX libraries that don't have this property (specifically JSX expressions can have arbitrary side effects and can't be removed when unused). If you are using such a library, you can use this setting to tell esbuild that JSX expressions have side effects:

CLI JS Go
esbuild app.jsx --jsx-side-effects
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.jsx'],
  outfile: 'out.js',
  jsxSideEffects: true,
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints:    []string{"app.jsx"},
    Outfile:        "out.js",
    JSXSideEffects: true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Supported

Supported by: Build and Transform

This setting lets you customize esbuild's set of unsupported syntax features at the individual syntax feature level. For example, you can use this to tell esbuild that BigInts are not supported so that esbuild generates an error when you try to use one. Usually this is configured for you when you use the target setting, which you should typically be using instead of this setting. If the target is specified in addition to this setting, this setting will override whatever is specified by the target.

Here are some examples of why you might want to use this setting instead of or in addition to setting the target:

If you want esbuild to consider a certain syntax feature to be unsupported, you can specify that like this:

CLI JS Go
esbuild app.js --supported:bigint=false
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  supported: {
    'bigint': false,
  },
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Supported: map[string]bool{
      "bigint": false,
    },
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Syntax features are specified using esbuild-specific feature names. The full set of feature names is as follows:

JavaScript:

CSS:

Target

Supported by: Build and Transform

This sets the target environment for the generated JavaScript and/or CSS code. It tells esbuild to transform JavaScript syntax that is too new for these environments into older JavaScript syntax that will work in these environments. For example, the ?? operator was introduced in Chrome 80 so esbuild will convert it into an equivalent (but more verbose) conditional expression when targeting Chrome 79 or earlier.

Note that this is only concerned with syntax features, not APIs. It does not automatically add polyfills for new APIs that are not used by these environments. You will have to explicitly import polyfills for the APIs you need (e.g. by importing core-js). Automatic polyfill injection is outside of esbuild's scope.

Each target environment is an environment name followed by a version number. The following environment names are currently supported:

In addition, you can also specify JavaScript language versions such as es2020. The default target is esnext which means that by default, esbuild will assume all of the latest JavaScript and CSS features are supported. Here is an example that configures multiple target environments. You don't need to specify all of them; you can just specify the subset of target environments that your project cares about. You can also be more precise about version numbers if you'd like (e.g. node12.19.0 instead of just node12):

CLI JS Go
esbuild app.js --target=es2020,chrome58,edge16,firefox57,node12,safari11
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  target: [
    'es2020',
    'chrome58',
    'edge16',
    'firefox57',
    'node12',
    'safari11',
  ],
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Target:      api.ES2020,
    Engines: []api.Engine{
      {Name: api.EngineChrome, Version: "58"},
      {Name: api.EngineEdge, Version: "16"},
      {Name: api.EngineFirefox, Version: "57"},
      {Name: api.EngineNode, Version: "12"},
      {Name: api.EngineSafari, Version: "11"},
    },
    Write: true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

You can refer to the JavaScript loader for the details about which syntax features were introduced with which language versions. Keep in mind that while JavaScript language versions such as es2020 are identified by year, that is the year the specification is approved. It has nothing to do with the year all major browsers implement that specification which often happens earlier or later than that year.

If you use a syntax feature that esbuild doesn't yet have support for transforming to your current language target, esbuild will generate an error where the unsupported syntax is used. This is often the case when targeting the es5 language version, for example, since esbuild only supports transforming most newer JavaScript syntax features to es6.

If you need to customize the set of supported syntax features at the individual feature level in addition to or instead of what target provides, you can do that with the supported setting.

Optimization

Define

Supported by: Build and Transform

This feature provides a way to replace global identifiers with constant expressions. It can be a way to change the behavior some code between builds without changing the code itself:

CLI JS Go
echo 'hooks = DEBUG && require("hooks")' | esbuild --define:DEBUG=true
hooks = require("hooks");
echo 'hooks = DEBUG && require("hooks")' | esbuild --define:DEBUG=false
hooks = false;
import * as esbuild from 'esbuild'let js = 'hooks = DEBUG && require("hooks")'(await esbuild.transform(js, {
  define: { DEBUG: 'true' },
})).code
'hooks = require("hooks");\n'
(await esbuild.transform(js, {
  define: { DEBUG: 'false' },
})).code
'hooks = false;\n'
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  js := "hooks = DEBUG && require('hooks')"

  result1 := api.Transform(js, api.TransformOptions{
    Define: map[string]string{"DEBUG": "true"},
  })

  if len(result1.Errors) == 0 {
    fmt.Printf("%s", result1.Code)
  }

  result2 := api.Transform(js, api.TransformOptions{
    Define: map[string]string{"DEBUG": "false"},
  })

  if len(result2.Errors) == 0 {
    fmt.Printf("%s", result2.Code)
  }
}

Each define entry maps an identifier to a string of code containing an expression. The expression in the string must either be a JSON object (null, boolean, number, string, array, or object) or a single identifier. Replacement expressions other than arrays and objects are substituted inline, which means that they can participate in constant folding. Array and object replacement expressions are stored in a variable and then referenced using an identifier instead of being substituted inline, which avoids substituting repeated copies of the value but means that the values don't participate in constant folding.

If you want to replace something with a string literal, keep in mind that the replacement value passed to esbuild must itself contain quotes because each define entry maps to a string containing code. Omitting the quotes means the replacement value is an identifier instead. This is demonstrated in the example below:

CLI JS Go
echo 'id, str' | esbuild --define:id=text --define:str=\"text\"
text, "text";
import * as esbuild from 'esbuild'(await esbuild.transform('id, str', {
  define: { id: 'text', str: '"text"' },
})).code
'text, "text";\n'
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  result := api.Transform("id, text", api.TransformOptions{
    Define: map[string]string{
      "id":  "text",
      "str": "\"text\"",
    },
  })

  if len(result.Errors) == 0 {
    fmt.Printf("%s", result.Code)
  }
}

If you're using the CLI, keep in mind that different shells have different rules for how to escape double-quote characters (which are necessary when the replacement value is a string). Use a \" backslash escape because it works in both bash and Windows command prompt. Other methods of escaping double quotes that work in bash such as surrounding them with single quotes will not work on Windows, since Windows command prompt does not remove the single quotes. This is relevant when using the CLI from a npm script in your package.json file, which people will expect to work on all platforms:

{
  "scripts": {
    "build": "esbuild --define:process.env.NODE_ENV=\\\"production\\\" app.js"
  }
}

If you still run into cross-platform quote escaping issues with different shells, you will probably want to switch to using the JavaScript API instead. There you can use regular JavaScript syntax to eliminate cross-platform differences.

If you're looking for a more advanced form of the define feature that can replace an expression with something other than a constant (e.g. replacing a global variable with a shim), you may be able to use the similar inject feature to do that.

Drop

Supported by: Build and Transform

This tells esbuild to edit your source code before building to drop certain constructs. There are currently two possible things that can be dropped:

CLI JS Go
esbuild app.js --drop:debugger
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  drop: ['debugger'],
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Drop:        api.DropDebugger,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}
CLI JS Go
esbuild app.js --drop:console
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  drop: ['console'],
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Drop:        api.DropConsole,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Drop labels

Supported by: Build and Transform

This tells esbuild to edit your source code before building to drop labeled statements with specific label names. For example, consider the following code:

function example() {
  DEV: doAnExpensiveCheck()
  return normalCodePath()
}

If you use this option to drop all labels named DEV, then esbuild will give you this:

function example() {
  return normalCodePath();
}

You can configure this feature like this (which will drop both the DEV and TEST labels):

CLI JS Go
esbuild app.js --drop-labels=DEV,TEST
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  dropLabels: ['DEV', 'TEST'],
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    DropLabels:  []string{"DEV", "TEST"},
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Note that this is not the only way to conditionally remove code. Another more common way is to use the define feature to replace specific global variables with a boolean value. For example, consider the following code:

function example() {
  DEV && doAnExpensiveCheck()
  return normalCodePath()
}

If you define DEV to false, then esbuild will give you this:

function example() {
  return normalCodePath();
}

This is pretty much the same thing as using a label. However, an advantage of using a label instead of a global variable to conditionally remove code is that you don't have to worry about the global variable not being defined because someone forgot to configure esbuild to replace it with something. Some drawbacks of using the label approach are that it makes conditionally removing code when the label is not dropped slightly harder to read, and it doesn't work for code embedded within nested expressions. Which approach to use for a given project comes down to personal preference.

Ignore annotations

Supported by: Build and Transform

Since JavaScript is a dynamic language, identifying unused code is sometimes very difficult for a compiler, so the community has developed certain annotations to help tell compilers what code should be considered side-effect free and available for removal. Currently there are two forms of side-effect annotations that esbuild supports:

These annotations can be problematic because the compiler depends completely on developers for accuracy, and developers occasionally publish packages with incorrect annotations. The sideEffects field is particularly error-prone for developers because by default it causes all files in your package to be considered dead code if no imports are used. If you add a new file containing side effects and forget to update that field, your package will likely break when people try to bundle it.

This is why esbuild includes a way to ignore side-effect annotations. You should only enable this if you encounter a problem where the bundle is broken because necessary code was unexpectedly removed from the bundle:

CLI JS Go
esbuild app.js --bundle --ignore-annotations
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  bundle: true,
  ignoreAnnotations: true,
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints:       []string{"app.js"},
    Bundle:            true,
    IgnoreAnnotations: true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Enabling this means esbuild will no longer respect /* @__PURE__ */ comments or the sideEffects field. It will still do automatic tree shaking of unused imports, however, since that doesn't rely on annotations from developers. Ideally this flag is only a temporary workaround. You should report these issues to the maintainer of the package to get them fixed since they indicate a problem with the package and they will likely trip up other people too.

Inject

Supported by: Build

This option allows you to automatically replace a global variable with an import from another file. This can be a useful tool for adapting code that you don't control to a new environment. For example, assume you have a file called process-cwd-shim.js that exports a shim using the export name process.cwd:

// process-cwd-shim.js
let processCwdShim = () => ''
export { processCwdShim as 'process.cwd' }
// entry.js
console.log(process.cwd())

This is intended to replace uses of node's process.cwd() function to prevent packages that call it from crashing when run in the browser. You can use the inject feature to replace all references to the global property process.cwd with an import from that file:

CLI JS Go
esbuild entry.js --inject:./process-cwd-shim.js --outfile=out.js
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['entry.js'],
  inject: ['./process-cwd-shim.js'],
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"entry.js"},
    Inject:      []string{"./process-cwd-shim.js"},
    Outfile:     "out.js",
    Write:       true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

That results in something like this:

// out.js
var processCwdShim = () => "";
console.log(processCwdShim());

You can think of the inject feature as similar to the define feature, except it replaces an expression with an import to a file instead of with a constant, and the expression to replace is specified using an export name in a file instead of using an inline string in esbuild's API.

Auto-import for JSX

React (the library for which JSX syntax was originally created) has a mode they call automatic where you don't have to import anything to use JSX syntax. Instead, the JSX-to-JS transformer will automatically import the correct JSX factory function for you. You can enable automatic JSX mode with esbuild's jsx setting. If you want auto-import for JSX and you are using a sufficiently new version of React, then you should be using the automatic JSX mode.

However, setting jsx to automatic unfortunately also means you are using a highly React-specific JSX transform instead of the default general-purpose JSX transform. This means writing a JSX factory function is more complicated, and it also means that the automatic mode doesn't work with libraries that expect to be used with the standard JSX transform (including older versions of React).

You can use esbuild's inject feature to automatically import the factory and fragment for JSX expressions when the JSX transform is not set to automatic. Here's an example file that can be injected to do this:

const { createElement, Fragment } = require('react')
export {
  createElement as 'React.createElement',
  Fragment as 'React.Fragment',
}

This code uses the React library as an example, but you can use this approach with any other JSX library as well with appropriate changes.

Injecting files without imports

You can also use this feature with files that have no exports. In that case the injected file just comes first before the rest of the output as if every input file contained import "./file.js". Because of the way ECMAScript modules work, this injection is still "hygienic" in that symbols with the same name in different files are renamed so they don't collide with each other.

Conditionally injecting a file

If you want to conditionally import a file only if the export is actually used, you should mark the injected file as not having side effects by putting it in a package and adding "sideEffects": false in that package's package.json file. This setting is a convention from Webpack that esbuild respects for any imported file, not just files used with inject.

Keep names

Supported by: Build and Transform

In JavaScript the name property on functions and classes defaults to a nearby identifier in the source code. These syntax forms all set the name property of the function to "fn":

function fn() {}
let fn = function() {};
fn = function() {};
let [fn = function() {}] = [];
let {fn = function() {}} = {};
[fn = function() {}] = [];
({fn = function() {}} = {});

However, minification renames symbols to reduce code size and bundling sometimes need to rename symbols to avoid collisions. That changes value of the name property for many of these cases. This is usually fine because the name property is normally only used for debugging. However, some frameworks rely on the name property for registration and binding purposes. If this is the case, you can enable this option to preserve the original name values even in minified code:

CLI JS Go
esbuild app.js --minify --keep-names
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  minify: true,
  keepNames: true,
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints:       []string{"app.js"},
    MinifyWhitespace:  true,
    MinifyIdentifiers: true,
    MinifySyntax:      true,
    KeepNames:         true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Note that this feature is unavailable if the target has been set to an old environment that doesn't allow esbuild to mutate the name property on functions and classes. This is the case for environments that don't support ES6.

Mangle props

Supported by: Build and Transform

Using this feature can break your code in subtle ways. Do not use this feature unless you know what you are doing, and you know exactly how it will affect both your code and all of your dependencies.

This setting lets you pass a regular expression to esbuild to tell esbuild to automatically rename all properties that match this regular expression. It's useful when you want to minify certain property names in your code either to make the generated code smaller or to somewhat obfuscate your code's intent.

Here's an example that uses the regular expression _$ to mangle all properties ending in an underscore, such as foo_. This mangles print({ foo_: 0 }.foo_) into print({ a: 0 }.a):

CLI JS Go
esbuild app.js --mangle-props=_$
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  mangleProps: /_$/,
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    MangleProps: "_$",
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Only mangling properties that end in an underscore is a reasonable heuristic because normal JS code doesn't typically contain identifiers like that. Browser APIs also don't use this naming convention so this also avoids conflicts with browser APIs. If you want to avoid mangling names such as __defineGetter__ you could consider using a more complex regular expression such as [^_]_$ (i.e. must end in a non-underscore followed by an underscore).

This is a separate setting instead of being part of the minify setting because it's an unsafe transformation that does not work on arbitrary JavaScript code. It only works if the provided regular expression matches all of the properties that you want mangled and does not match any of the properties that you don't want mangled. It also only works if you do not under any circumstances reference a mangled property indirectly. For example, it means you can't use obj[prop] to reference a property where prop is a string containing the property name. Specifically the following syntax constructs are the only ones eligible for property mangling:

Syntax Example
Dot property accesses x.foo_
Dot optional chains x?.foo_
Object properties x = { foo_: y }
Object methods x = { foo_() {} }
Class fields class x { foo_ = y }
Class methods class x { foo_() {} }
Object destructuring bindings let { foo_: x } = y
Object destructuring assignments ({ foo_: x } = y)
JSX element member expression <X.foo_></X.foo_>
JSX attribute names <X foo_={y} />
TypeScript namespace exports namespace x { export let foo_ = y }
TypeScript parameter properties class x { constructor(public foo_) {} }

When using this feature, keep in mind that property names are only consistently mangled within a single esbuild API call but not across esbuild API calls. Each esbuild API call does an independent property mangling operation so output files generated by two different API calls may mangle the same property to two different names, which could cause the resulting code to behave incorrectly.

Quoted properties

By default, esbuild doesn't modify the contents of string literals. This means you can avoid property mangling for an individual property by quoting it as a string. However, you must consistently use quotes or no quotes for a given property everywhere for this to work. For example, print({ foo_: 0 }.foo_) will be mangled into print({ a: 0 }.a) while print({ 'foo_': 0 }['foo_']) will not be mangled.

If you would like for esbuild to also mangle the contents of string literals, you can explicitly enable that behavior like this:

CLI JS Go
esbuild app.js --mangle-props=_$ --mangle-quoted
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  mangleProps: /_$/,
  mangleQuoted: true,
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints:  []string{"app.js"},
    MangleProps:  "_$",
    MangleQuoted: api.MangleQuotedTrue,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Enabling this makes the following syntax constructs also eligible for property mangling:

Syntax Example
Quoted property accesses x['foo_']
Quoted optional chains x?.['foo_']
Quoted object properties x = { 'foo_': y }
Quoted object methods x = { 'foo_'() {} }
Quoted class fields class x { 'foo_' = y }
Quoted class methods class x { 'foo_'() {} }
Quoted object destructuring bindings let { 'foo_': x } = y
Quoted object destructuring assignments ({ 'foo_': x } = y)
String literals to the left of in 'foo_' in x

Mangling other strings

Mangling quoted properties still only mangles strings in property name position. Sometimes you may also need to mangle property names in strings at arbitrary other locations in your code. To do that, you can prefix the string with a /* @__KEY__ */ comment to tell esbuild that the contents of a string should be treated as a property name that can be mangled. For example:

let obj = {}
Object.defineProperty(
  obj,
  /* @__KEY__ */ 'foo_',
  { get: () => 123 },
)
console.log(obj.foo_)

This will cause the contents of the string 'foo_' to be mangled as a property name (assuming property mangling is enabled and foo_ is eligible for renaming). The /* @__KEY__ */ comment is a convention from Terser, a popular JavaScript minifier with a similar property mangling feature.

Preventing renaming

If you would like to exclude certain properties from mangling, you can reserve them with an additional setting. For example, this uses the regular expression ^__.*__$ to reserve all properties that start and end with two underscores, such as __foo__:

CLI JS Go
esbuild app.js --mangle-props=_$ "--reserve-props=^__.*__$"
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  mangleProps: /_$/,
  reserveProps: /^__.*__$/,
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints:  []string{"app.js"},
    MangleProps:  "_$",
    ReserveProps: "^__.*__$",
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Persisting renaming decisions

Advanced usage of the property mangling feature involves storing the mapping from original name to mangled name in a persistent cache. When enabled, all mangled property renamings are recorded in the cache during the initial build. Subsequent builds reuse the renamings stored in the cache and add additional renamings for any newly-added properties. This has a few consequences:

For example, consider the following input file:

console.log({
  someProp_: 1,
  customRenaming_: 2,
  disabledRenaming_: 3
});

If we want customRenaming_ to be renamed to cR_ and we don't want disabledRenaming_ to be renamed at all, we can pass the following mangle cache JSON to esbuild:

{
  "customRenaming_": "cR_",
  "disabledRenaming_": false
}

The mangle cache JSON can be passed to esbuild like this:

CLI JS Go
esbuild app.js --mangle-props=_$ --mangle-cache=cache.json
import * as esbuild from 'esbuild'

let result = await esbuild.build({
  entryPoints: ['app.js'],
  mangleProps: /_$/,
  mangleCache: {
    customRenaming_: "cR_",
    disabledRenaming_: false
  },
})

console.log('updated mangle cache:', result.mangleCache)
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    MangleProps: "_$",
    MangleCache: map[string]interface{}{
      "customRenaming_":   "cR_",
      "disabledRenaming_": false,
    },
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }

  fmt.Println("updated mangle cache:", result.MangleCache)
}

When property naming is enabled, that will result in the following output file:

console.log({
  a: 1,
  cR_: 2,
  disabledRenaming_: 3
});

And the following updated mangle cache:

{
  "customRenaming_": "cR_",
  "disabledRenaming_": false,
  "someProp_": "a"
}

Minify

Supported by: Build and Transform

When enabled, the generated code will be minified instead of pretty-printed. Minified code is generally equivalent to non-minified code but is smaller, which means it downloads faster but is harder to debug. Usually you minify code in production but not in development.

Enabling minification in esbuild looks like this:

CLI JS Go
echo 'fn = obj => { return obj.x }' | esbuild --minify
fn=n=>n.x;
import * as esbuild from 'esbuild'var js = 'fn = obj => { return obj.x }'
(await esbuild.transform(js, {
  minify: true,
})).code
'fn=n=>n.x;\n'
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  js := "fn = obj => { return obj.x }"

  result := api.Transform(js, api.TransformOptions{
    MinifyWhitespace:  true,
    MinifyIdentifiers: true,
    MinifySyntax:      true,
  })

  if len(result.Errors) == 0 {
    fmt.Printf("%s", result.Code)
  }
}

This option does three separate things in combination: it removes whitespace, it rewrites your syntax to be more compact, and it renames local variables to be shorter. Usually you want to do all of these things, but these options can also be enabled individually if necessary:

CLI JS Go
echo 'fn = obj => { return obj.x }' | esbuild --minify-whitespace
fn=obj=>{return obj.x};
echo 'fn = obj => { return obj.x }' | esbuild --minify-identifiers
fn = (n) => {
  return n.x;
};
echo 'fn = obj => { return obj.x }' | esbuild --minify-syntax
fn = (obj) => obj.x;
import * as esbuild from 'esbuild'var js = 'fn = obj => { return obj.x }'
(await esbuild.transform(js, {
  minifyWhitespace: true,
})).code
'fn=obj=>{return obj.x};\n'
(await esbuild.transform(js, {
  minifyIdentifiers: true,
})).code
'fn = (n) => {\n  return n.x;\n};\n'
(await esbuild.transform(js, {
  minifySyntax: true,
})).code
'fn = (obj) => obj.x;\n'
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  css := "div { color: yellow }"

  result1 := api.Transform(css, api.TransformOptions{
    Loader:           api.LoaderCSS,
    MinifyWhitespace: true,
  })

  if len(result1.Errors) == 0 {
    fmt.Printf("%s", result1.Code)
  }

  result2 := api.Transform(css, api.TransformOptions{
    Loader:            api.LoaderCSS,
    MinifyIdentifiers: true,
  })

  if len(result2.Errors) == 0 {
    fmt.Printf("%s", result2.Code)
  }

  result3 := api.Transform(css, api.TransformOptions{
    Loader:       api.LoaderCSS,
    MinifySyntax: true,
  })

  if len(result3.Errors) == 0 {
    fmt.Printf("%s", result3.Code)
  }
}

These same concepts also apply to CSS, not just to JavaScript:

CLI JS Go
echo 'div { color: yellow }' | esbuild --loader=css --minify
div{color:#ff0}
import * as esbuild from 'esbuild'var css = 'div { color: yellow }'
(await esbuild.transform(css, {
  loader: 'css',
  minify: true,
})).code
'div{color:#ff0}\n'
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  css := "div { color: yellow }"

  result := api.Transform(css, api.TransformOptions{
    Loader:            api.LoaderCSS,
    MinifyWhitespace:  true,
    MinifyIdentifiers: true,
    MinifySyntax:      true,
  })

  if len(result.Errors) == 0 {
    fmt.Printf("%s", result.Code)
  }
}

The JavaScript minification algorithm in esbuild usually generates output that is very close to the minified output size of industry-standard JavaScript minification tools. This benchmark has an example comparison of output sizes between different minifiers. While esbuild is not the optimal JavaScript minifier in all cases (and doesn't try to be), it strives to generate minified output within a few percent of the size of dedicated minification tools for most code, and of course to do so much faster than other tools.

Considerations

Here are some things to keep in mind when using esbuild as a minifier:

Pure

Supported by: Build and Transform

There is a convention used by various JavaScript tools where a special comment containing either /* @__PURE__ */ or /* #__PURE__ */ before a new or call expression means that that expression can be removed if the resulting value is unused. It looks like this:

let button = /* @__PURE__ */ React.createElement(Button, null);

This information is used by bundlers such as esbuild during tree shaking (a.k.a. dead code removal) to perform fine-grained removal of unused imports across module boundaries in situations where the bundler is not able to prove by itself that the removal is safe due to the dynamic nature of JavaScript code.

Note that while the comment says "pure", it confusingly does not indicate that the function being called is pure. For example, it does not indicate that it is ok to cache repeated calls to that function. The name is essentially just an abstract shorthand for "ok to be removed if unused".

Some expressions such as JSX and certain built-in globals are automatically annotated as /* @__PURE__ */ in esbuild. You can also configure additional globals to be marked /* @__PURE__ */ as well. For example, you can mark the global document.createElement function as such to have it be automatically removed from your bundle when the bundle is minified as long as the result isn't used.

It's worth mentioning that the effect of the annotation only extends to the call itself, not to the arguments. Arguments with side effects are still kept even when minification is enabled:

CLI JS Go
echo 'document.createElement(elemName())' | esbuild --pure:document.createElement
/* @__PURE__ */ document.createElement(elemName());
echo 'document.createElement(elemName())' | esbuild --pure:document.createElement --minify
elemName();
import * as esbuild from 'esbuild'let js = 'document.createElement(elemName())'
(await esbuild.transform(js, {
  pure: ['document.createElement'],
})).code
'/* @__PURE__ */ document.createElement(elemName());\n'
(await esbuild.transform(js, {
  pure: ['document.createElement'],
  minify: true,
})).code
'elemName();\n'
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  js := "document.createElement(elemName())"

  result1 := api.Transform(js, api.TransformOptions{
    Pure: []string{"document.createElement"},
  })

  if len(result1.Errors) == 0 {
    fmt.Printf("%s", result1.Code)
  }

  result2 := api.Transform(js, api.TransformOptions{
    Pure:         []string{"document.createElement"},
    MinifySyntax: true,
  })

  if len(result2.Errors) == 0 {
    fmt.Printf("%s", result2.Code)
  }
}

Note that if you are trying to remove all calls to console API methods such as console.log and also want to remove the evaluation of arguments with side effects, there is a special case available for this: you can use the drop feature instead of marking console API calls as pure. However, this mechanism is specific to the console API and doesn't work with other call expressions.

Tree shaking

Supported by: Build and Transform

Tree shaking is the term the JavaScript community uses for dead code elimination, a common compiler optimization that automatically removes unreachable code. Within esbuild, this term specifically refers to declaration-level dead code removal.

Tree shaking is easiest to explain with an example. Consider the following file. There is one used function and one unused function:

// input.js
function one() {
  console.log('one')
}
function two() {
  console.log('two')
}
one()

If you bundle this file with esbuild --bundle input.js --outfile=output.js, the unused function will automatically be discarded leaving you with the following output:

// input.js
function one() {
  console.log("one");
}
one();

This even works if we split our functions off into a separate library file and import them using an import statement:

// lib.js
export function one() {
  console.log('one')
}
export function two() {
  console.log('two')
}
// input.js
import * as lib from './lib.js'
lib.one()

If you bundle this file with esbuild --bundle input.js --outfile=output.js, the unused function and unused import will still be automatically discarded leaving you with the following output:

// lib.js
function one() {
  console.log("one");
}

// input.js
one();

This way esbuild will only bundle the parts of your packages that you actually use, which can sometimes be a substantial size savings. Note that esbuild's tree shaking implementation relies on the use of ECMAScript module import and export statements. It does not work with CommonJS modules. Many packages on npm include both formats and esbuild tries to pick the format that works with tree shaking by default. You can customize which format esbuild picks using the main fields and/or conditions options depending on the package.

By default, tree shaking is only enabled either when bundling is enabled or when the output format is set to iife, otherwise tree shaking is disabled. You can force-enable tree shaking by setting it to true:

CLI JS Go
esbuild app.js --tree-shaking=true
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  treeShaking: true,
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    TreeShaking: api.TreeShakingTrue,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

You can also force-disable tree shaking by setting it to false:

CLI JS Go
esbuild app.js --tree-shaking=false
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  treeShaking: false,
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    TreeShaking: api.TreeShakingFalse,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Tree shaking and side effects

The side effect detection used for tree shaking is conservative, meaning that esbuild only considers code removable as dead code if it can be sure that there are no hidden side effects. For example, primitive literals such as 12.34 and "abcd" are side-effect free and can be removed while expressions such as "ab" + cd and foo.bar are not side-effect free (joining strings invokes toString() which can have side effects, and member access can invoke a getter which can also have side effects). Even referencing a global identifier is considered to be a side effect because it will throw a ReferenceError if there is no global with that name. Here's an example:

// These are considered side-effect free
let a = 12.34;
let b = "abcd";
let c = { a: a };

// These are not considered side-effect free
// since they could cause some code to run
let x = "ab" + cd;
let y = foo.bar;
let z = { [x]: x };

Sometimes it's desirable to allow some code to be tree shaken even if that code can't be automatically determined to have no side effects. This can be done with a pure annotation comment which tells esbuild to trust the author of the code that there are no side effects within the annotated code. The annotation comment is /* @__PURE__ */ and can only precede a new or call expression. You can annotate an immediately-invoked function expression and put arbitrary side effects inside the function body:

// This is considered side-effect free due to
// the annotation, and will be removed if unused
let gammaTable = /* @__PURE__ */ (() => {
  // Side-effect detection is skipped in here
  let table = new Uint8Array(256);
  for (let i = 0; i < 256; i++)
    table[i] = Math.pow(i / 255, 2.2) * 255;
  return table;
})();

While the fact that /* @__PURE__ */ only works on call expressions can sometimes make code more verbose, a big benefit of this syntax is that it's portable across many other tools in the JavaScript ecosystem including the popular UglifyJS and Terser JavaScript minifiers (which are used by other major tools including Webpack and Parcel).

Note that the annotations cause esbuild to assume that the annotated code is side-effect free. If the annotations are wrong and the code actually does have important side effects, these annotations can result in broken code. If you are bundling third-party code with annotations that have been authored incorrectly, you may need to enable ignoring annotations to make sure the bundled code is correct.

Source maps

Source root

Supported by: Build and Transform

This feature is only relevant when source maps are enabled. It lets you set the value of the sourceRoot field in the source map, which specifies the path that all other paths in the source map are relative to. If this field is not present, all paths in the source map are interpreted as being relative to the directory containing the source map instead.

You can configure sourceRoot like this:

CLI JS Go
esbuild app.js --sourcemap --source-root=https://raw.githubusercontent.com/some/repo/v1.2.3/
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  sourcemap: true,
  sourceRoot: 'https://raw.githubusercontent.com/some/repo/v1.2.3/',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Sourcemap:   api.SourceMapInline,
    SourceRoot:  "https://raw.githubusercontent.com/some/repo/v1.2.3/",
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Sourcefile

Supported by: Build and Transform

This option sets the file name when using an input which has no file name. This happens when using the transform API and when using the build API with stdin. The configured file name is reflected in error messages and in source maps. If it's not configured, the file name defaults to <stdin>. It can be configured like this:

CLI JS Go
cat app.js | esbuild --sourcefile=example.js --sourcemap
import * as esbuild from 'esbuild'
import fs from 'node:fs'

let js = fs.readFileSync('app.js', 'utf8')
let result = await esbuild.transform(js, {
  sourcefile: 'example.js',
  sourcemap: 'inline',
})

console.log(result.code)
package main

import "fmt"
import "io/ioutil"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  js, err := ioutil.ReadFile("app.js")
  if err != nil {
    panic(err)
  }

  result := api.Transform(string(js),
    api.TransformOptions{
      Sourcefile: "example.js",
      Sourcemap:  api.SourceMapInline,
    })

  if len(result.Errors) == 0 {
    fmt.Printf("%s %s", result.Code)
  }
}

Sourcemap

Supported by: Build and Transform

Source maps can make it easier to debug your code. They encode the information necessary to translate from a line/column offset in a generated output file back to a line/column offset in the corresponding original input file. This is useful if your generated code is sufficiently different from your original code (e.g. your original code is TypeScript or you enabled minification). This is also useful if you prefer looking at individual files in your browser's developer tools instead of one big bundled file.

Note that source map output is supported for both JavaScript and CSS, and the same options apply to both. Everything below that talks about .js files also applies similarly to .css files.

There are four different modes for source map generation:

  1. linked

    This mode means the source map is generated into a separate .js.map output file alongside the .js output file, and the .js output file contains a special //# sourceMappingURL= comment that points to the .js.map output file. That way the browser knows where to find the source map for a given file when you open the debugger. Use linked source map mode like this:

CLI JS Go
esbuild app.ts --sourcemap --outfile=out.js
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.ts'],
  sourcemap: true,
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.ts"},
    Sourcemap:   api.SourceMapLinked,
    Outfile:     "out.js",
    Write:       true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}
  1. external

    This mode means the source map is generated into a separate .js.map output file alongside the .js output file, but unlike linked mode the .js output file does not contain a //# sourceMappingURL= comment. Use external source map mode like this:

CLI JS Go
esbuild app.ts --sourcemap=external --outfile=out.js
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.ts'],
  sourcemap: 'external',
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.ts"},
    Sourcemap:   api.SourceMapExternal,
    Outfile:     "out.js",
    Write:       true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}
  1. inline

    This mode means the source map is appended to the end of the .js output file as a base64 payload inside a //# sourceMappingURL= comment. No additional .js.map output file is generated. Keep in mind that source maps are usually very big because they contain all of your original source code, so you usually do not want to ship code containing inline source maps. To remove the source code from the source map (keeping only the file names and the line/column mappings), use the sources content option. Use inline source map mode like this:

CLI JS Go
esbuild app.ts --sourcemap=inline --outfile=out.js
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.ts'],
  sourcemap: 'inline',
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.ts"},
    Sourcemap:   api.SourceMapInline,
    Outfile:     "out.js",
    Write:       true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}
  1. both

    This mode is a combination of inline and external. The source map is appended inline to the end of the .js output file, and another copy of the same source map is written to a separate .js.map output file alongside the .js output file. Use both source map mode like this:

CLI JS Go
esbuild app.ts --sourcemap=both --outfile=out.js
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.ts'],
  sourcemap: 'both',
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.ts"},
    Sourcemap:   api.SourceMapInlineAndExternal,
    Outfile:     "out.js",
    Write:       true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

The build API supports all four source map modes listed above, but the transform API does not support the linked mode. This is because the output returned from the transform API does not have an associated filename. If you want the output of the transform API to have a source map comment, you can append one yourself. In addition, the CLI form of the transform API only supports the inline mode because the output is written to stdout so generating multiple output files is not possible.

If you want to "peek under the hood" to see what a source map does (or to debug problems with your source map), you can upload the relevant output file and the associated source map here: Source Map Visualization.

Using source maps

In the browser, source maps should be automatically picked up by the browser's developer tools as long as the source map setting is enabled. Note that the browser only uses the source maps to alter the display of stack traces when they are logged to the console. The stack traces themselves are not modified so inspecting error.stack in your code will still give the unmapped stack trace containing compiled code. Here's how to enable this setting in your browser's developer tools:

In node, source maps are supported natively starting with version v12.12.0. This feature is disabled by default but can be enabled with a flag. Unlike in the browser, the actual stack traces are also modified in node so inspecting error.stack in your code will give the mapped stack trace containing your original source code. Here's how to enable this setting in node (the --enable-source-maps flag must come before the script file name):

node --enable-source-maps app.js

Sources content

Supported by: Build and Transform

Source maps are generated using version 3 of the source map format, which is by far the most widely-supported variant. Each source map will look something like this:

{
  "version": 3,
  "sources": ["bar.js", "foo.js"],
  "sourcesContent": ["bar()", "foo()\nimport './bar'"],
  "mappings": ";AAAA;;;ACAA;",
  "names": []
}

The sourcesContent field is an optional field that contains all of the original source code. This is helpful for debugging because it means the original source code will be available in the debugger.

However, it's not needed in some scenarios. For example, if you are just using source maps in production to generate stack traces that contain the original file name, you don't need the original source code because there is no debugger involved. In that case it can be desirable to omit the sourcesContent field to make the source map smaller:

CLI JS Go
esbuild --bundle app.js --sourcemap --sources-content=false
import * as esbuild from 'esbuild'

await esbuild.build({
  bundle: true,
  entryPoints: ['app.js'],
  sourcemap: true,
  sourcesContent: false,
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    Bundle:         true,
    EntryPoints:    []string{"app.js"},
    Sourcemap:      api.SourceMapInline,
    SourcesContent: api.SourcesContentExclude,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Build metadata

Analyze

Supported by: Build

If you're looking for an interactive visualization, try esbuild's Bundle Size Analyzer instead. You can upload your esbuild metafile to see a bundle size breakdown.

Using the analyze feature generates an easy-to-read report about the contents of your bundle:

CLI JS Go
esbuild --bundle example.jsx --outfile=out.js --minify --analyze

  out.js                                                                    27.6kb  100.0%
   ├ node_modules/react-dom/cjs/react-dom-server.browser.production.min.js  19.2kb   69.8%
   ├ node_modules/react/cjs/react.production.min.js                          5.9kb   21.4%
   ├ node_modules/object-assign/index.js                                     962b     3.4%
   ├ example.jsx                                                             137b     0.5%
   ├ node_modules/react-dom/server.browser.js                                 50b     0.2%
   └ node_modules/react/index.js                                              50b     0.2%

...
import * as esbuild from 'esbuild'

let result = await esbuild.build({
  entryPoints: ['example.jsx'],
  outfile: 'out.js',
  minify: true,
  metafile: true,
})

console.log(await esbuild.analyzeMetafile(result.metafile))
package main

import "github.com/evanw/esbuild/pkg/api"
import "fmt"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints:       []string{"example.jsx"},
    Outfile:           "out.js",
    MinifyWhitespace:  true,
    MinifyIdentifiers: true,
    MinifySyntax:      true,
    Metafile:          true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }

  fmt.Printf("%s", api.AnalyzeMetafile(result.Metafile, api.AnalyzeMetafileOptions{}))
}

The information shows which input files ended up in each output file as well as the percentage of the output file they ended up taking up. If you would like additional information, you can enable the "verbose" mode. This currently shows the import path from the entry point to each input file which tells you why a given input file is being included in the bundle:

CLI JS Go
esbuild --bundle example.jsx --outfile=out.js --minify --analyze=verbose

  out.js ─────────────────────────────────────────────────────────────────── 27.6kb ─ 100.0%
   ├ node_modules/react-dom/cjs/react-dom-server.browser.production.min.js ─ 19.2kb ── 69.8%
   │  └ node_modules/react-dom/server.browser.js
   │     └ example.jsx
   ├ node_modules/react/cjs/react.production.min.js ───────────────────────── 5.9kb ── 21.4%
   │  └ node_modules/react/index.js
   │     └ example.jsx
   ├ node_modules/object-assign/index.js ──────────────────────────────────── 962b ──── 3.4%
   │  └ node_modules/react-dom/cjs/react-dom-server.browser.production.min.js
   │     └ node_modules/react-dom/server.browser.js
   │        └ example.jsx
   ├ example.jsx ──────────────────────────────────────────────────────────── 137b ──── 0.5%
   ├ node_modules/react-dom/server.browser.js ──────────────────────────────── 50b ──── 0.2%
   │  └ example.jsx
   └ node_modules/react/index.js ───────────────────────────────────────────── 50b ──── 0.2%
      └ example.jsx

...
import * as esbuild from 'esbuild'

let result = await esbuild.build({
  entryPoints: ['example.jsx'],
  outfile: 'out.js',
  minify: true,
  metafile: true,
})

console.log(await esbuild.analyzeMetafile(result.metafile, {
  verbose: true,
}))
package main

import "github.com/evanw/esbuild/pkg/api"
import "fmt"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints:       []string{"example.jsx"},
    Outfile:           "out.js",
    MinifyWhitespace:  true,
    MinifyIdentifiers: true,
    MinifySyntax:      true,
    Metafile:          true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }

  fmt.Printf("%s", api.AnalyzeMetafile(result.Metafile, api.AnalyzeMetafileOptions{
    Verbose: true,
  }))
}

This analysis is just a visualization of the information that can be found in the metafile. If this analysis doesn't exactly suit your needs, you are welcome to build your own visualization using the information in the metafile.

Note that this formatted analysis summary is intended for humans, not machines. The specific formatting may change over time which will likely break any tools that try to parse it. You should not write a tool to parse this data. You should be using the information in the JSON metadata file instead. Everything in this visualization is derived from the JSON metadata so you are not losing out on any information by not parsing esbuild's formatted analysis summary.

Metafile

Supported by: Build

This option tells esbuild to produce some metadata about the build in JSON format. The following example puts the metadata in a file called meta.json:

CLI JS Go
esbuild app.js --bundle --metafile=meta.json --outfile=out.js
import * as esbuild from 'esbuild'
import fs from 'node:fs'

let result = await esbuild.build({
  entryPoints: ['app.js'],
  bundle: true,
  metafile: true,
  outfile: 'out.js',
})

fs.writeFileSync('meta.json', JSON.stringify(result.metafile))
package main

import "io/ioutil"
import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    Metafile:    true,
    Outfile:     "out.js",
    Write:       true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }

  ioutil.WriteFile("meta.json", []byte(result.Metafile), 0644)
}

This data can then be analyzed by other tools. For an interactive visualization, you can use esbuild's own Bundle Size Analyzer. For a quick textual analysis, you can use esbuild's build-in analyze feature. Or you can write your own analysis which uses this information.

The metadata JSON format looks like this (described using a TypeScript interface):

interface Metafile {
  inputs: {
    [path: string]: {
      bytes: number
      imports: {
        path: string
        kind: string
        external?: boolean
        original?: string
        with?: Record<string, string>
      }[]
      format?: string
      with?: Record<string, string>
    }
  }
  outputs: {
    [path: string]: {
      bytes: number
      inputs: {
        [path: string]: {
          bytesInOutput: number
        }
      }
      imports: {
        path: string
        kind: string
        external?: boolean
      }[]
      exports: string[]
      entryPoint?: string
      cssBundle?: string
    }
  }
}

Logging

Color

Supported by: Build and Transform

This option enables or disables colors in the error and warning messages that esbuild writes to stderr file descriptor in the terminal. By default, color is automatically enabled if stderr is a TTY session and automatically disabled otherwise. Colored output in esbuild looks like this:

[WARNING] The "typeof" operator will never evaluate to "null" [impossible-typeof]

    example.js:2:16:
      2 │ log(typeof x == "null")
        ╵                 ~~~~~~

  The expression "typeof x" actually evaluates to "object" in JavaScript, not "null". You need to
  use "x === null" to test for null.

[ERROR] Could not resolve "logger"

    example.js:1:16:
      1 │ import log from "logger"~~~~~~~~

  You can mark the path "logger" as external to exclude it from the bundle, which will remove this
  error and leave the unresolved path in the bundle.

Colored output can be force-enabled by setting color to true. This is useful if you are piping esbuild's stderr output into a TTY yourself:

CLI JS Go
echo 'typeof x == "null"' | esbuild --color=true 2> stderr.txt
import * as esbuild from 'esbuild'

let js = 'typeof x == "null"'
await esbuild.transform(js, {
  color: true,
})
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  js := "typeof x == 'null'"

  result := api.Transform(js, api.TransformOptions{
    Color: api.ColorAlways,
  })

  if len(result.Errors) == 0 {
    fmt.Printf("%s", result.Code)
  }
}

Colored output can also be set to false to disable colors.

Format messages

Supported by: Build and Transform

This API call can be used to format the log errors and warnings returned by the build API and transform APIs as a string using the same formatting that esbuild itself uses. This is useful if you want to customize the way esbuild's logging works, such as processing the log messages before they are printed or printing them to somewhere other than to the console. Here's an example:

JS Go
import * as esbuild from 'esbuild'

let formatted = await esbuild.formatMessages([
  {
    text: 'This is an error',
    location: {
      file: 'app.js',
      line: 10,
      column: 4,
      length: 3,
      lineText: 'let foo = bar',
    },
  },
], {
  kind: 'error',
  color: false,
  terminalWidth: 100,
})

console.log(formatted.join('\n'))
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"
import "strings"

func main() {
  formatted := api.FormatMessages([]api.Message{
    {
      Text: "This is an error",
      Location: &api.Location{
        File:     "app.js",
        Line:     10,
        Column:   4,
        Length:   3,
        LineText: "let foo = bar",
      },
    },
  }, api.FormatMessagesOptions{
    Kind:          api.ErrorMessage,
    Color:         false,
    TerminalWidth: 100,
  })

  fmt.Printf("%s", strings.Join(formatted, "\n"))
}

Options

The following options can be provided to control the formatting:

JS Go
interface FormatMessagesOptions {
  kind: 'error' | 'warning';
  color?: boolean;
  terminalWidth?: number;
}
type FormatMessagesOptions struct {
  Kind          MessageKind
  Color         bool
  TerminalWidth int
}

Log level

Supported by: Build and Transform

The log level can be changed to prevent esbuild from printing warning and/or error messages to the terminal. The six log levels are:

The log level can be set like this:

CLI JS Go
echo 'typeof x == "null"' | esbuild --log-level=error
import * as esbuild from 'esbuild'

let js = 'typeof x == "null"'
await esbuild.transform(js, {
  logLevel: 'error',
})
package main

import "fmt"
import "github.com/evanw/esbuild/pkg/api"

func main() {
  js := "typeof x == 'null'"

  result := api.Transform(js, api.TransformOptions{
    LogLevel: api.LogLevelError,
  })

  if len(result.Errors) == 0 {
    fmt.Printf("%s", result.Code)
  }
}

Log limit

Supported by: Build and Transform

By default, esbuild stops reporting log messages after 10 messages have been reported. This avoids the accidental generation of an overwhelming number of log messages, which can easily lock up slower terminal emulators such as Windows command prompt. It also avoids accidentally using up the whole scroll buffer for terminal emulators with limited scroll buffers.

The log limit can be changed to another value, and can also be disabled completely by setting it to zero. This will show all log messages:

CLI JS Go
esbuild app.js --log-limit=0
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  logLimit: 0,
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    LogLimit:    0,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Log override

Supported by: Build and Transform

This feature lets you change the log level of individual types of log messages. You can use it to silence a particular type of warning, to enable additional warnings that aren't enabled by default, or even to turn warnings into errors.

For example, when targeting older browsers, esbuild automatically transforms regular expression literals which use features that are too new for those browsers into new RegExp() calls to allow the generated code to run without being considered a syntax error by the browser. However, these calls will still throw at runtime if you don't add a polyfill for RegExp because that regular expression syntax is still unsupported. If you want esbuild to generate a warning when you use newer unsupported regular expression syntax, you can do that like this:

CLI JS Go
esbuild app.js --log-override:unsupported-regexp=warning --target=chrome50
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['app.js'],
  logOverride: {
    'unsupported-regexp': 'warning',
  },
  target: 'chrome50',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    LogOverride: map[string]api.LogLevel{
      "unsupported-regexp": api.LogLevelWarning,
    },
    Engines: []api.Engine{
      {Name: api.EngineChrome, Version: "50"},
    },
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

The log level for each message type can be overridden to any value supported by the log level setting. All currently-available message types are listed below (click on each one for an example log message):

These message types should be reasonably stable but new ones may be added and old ones may occasionally be removed in the future. If a message type is removed, any overrides for that message type will just be silently ignored.