W3cubDocs

/esbuild

API

The API can be accessed in one of three ways: 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.

There are two main API calls in esbuild's API: transform and build. It's important to understand which one you should be using because they work differently.

If you are using JavaScript be sure to check out the JS-specific details section below. You may also find the TypeScript type definitions for esbuild helpful as a reference. If you are using Go be sure to check out the automatically generated Go documentation.

If you are using the command-line API, it may be helpful to know that the flags come in one of three forms: --foo, --foo=bar, or --foo:bar. The form --foo is used for enabling boolean flags such as --minify, the form --foo=bar is used for flags that have a single value and are only specified once such as --platform=, and the form --foo:bar is used for flags that have multiple values and can be re-specified multiple times such as --external:.

Transform API

The transform API call operates on a single string without access to a file system. This makes it ideal for use in environments without a file system (such as a browser) or as part of another tool chain. Here is what a simple transform looks like:

echo 'let x: number = 1' | esbuild --loader=tslet x = 1;
require('esbuild').transformSync('let x: number = 1', {
  loader: 'ts',
}){
  code: 'let x = 1;\n',
  map: '',
  warnings: []
}
package main

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

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

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

This API call is used by the command-line interface if no input files are provided and the --bundle flag is not present. In this case the input string comes from stdin and the output string goes to stdout. The transform API can take the following options:

Simple options:

Advanced options:

Build API

The build API call operates on one or more files in the file system. This allows the files to reference each other and be bundled together. Here is what a simple build looks like:

echo 'let x: number = 1' > in.tsesbuild in.ts --outfile=out.jscat out.jslet x = 1;
require('fs').writeFileSync('in.ts', 'let x: number = 1')require('esbuild').buildSync({
  entryPoints: ['in.ts'],
  outfile: 'out.js',
}){ errors: [], warnings: [] }require('fs').readFileSync('out.js', 'utf8')'let x = 1;\n'
package main

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

func main() {
  ioutil.WriteFile("in.ts", []byte("let x: number = 1"), 0644)

  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"in.ts"},
    Outfile:     "out.js",
    Write:       true,
  })

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

This API call is used by the command-line interface if there is at least one input file provided or the --bundle flag is present. Note that esbuild does not bundle by default. You have to explicitly pass the --bundle flag to enable bundling. If no input files are provided then a single input file is read from stdin. The build API can take the following options:

Simple options:

Advanced options:

Simple 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:

esbuild in.js --bundle
require('esbuild').buildSync({
  entryPoints: ['in.js'],
  bundle: true,
  outfile: 'out.js',
}){ errors: [], warnings: [] }
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

Bundling with esbuild only works with statically-defined imports (i.e. when the import path is a string literal). Imports that are defined at run-time (i.e. imports that depend on run-time code evaluation) are not bundled, since bundling is a compile-time operation. For example:

// Analyzable imports (will be bundled by esbuild)
import 'pkg';
import('pkg');
require('pkg');

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

The way to work around this issue 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 this 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.

Define

Supported by: Transform | Build

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:

echo 'DEBUG && require("hooks")' | esbuild --define:DEBUG=truerequire("hooks");echo 'DEBUG && require("hooks")' | esbuild --define:DEBUG=falsefalse;
let js = 'DEBUG && require("hooks")'require('esbuild').transformSync(js, {
  define: { DEBUG: 'true' },
}){
  code: 'require("hooks");\n',
  map: '',
  warnings: []
}require('esbuild').transformSync(js, {
  define: { DEBUG: 'false' },
}){
  code: 'false;\n',
  map: '',
  warnings: []
}
package main

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

func main() {
  js := "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)
  }
}

Replacement expressions 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. Omitting the quotes means the replacement value is an identifier instead:

echo 'id, str' | esbuild --define:id=text --define:str=\"text\"text, "text";
require('esbuild').transformSync('id, str', {
  define: { id: 'text', str: '"text"' },
}){
  code: 'text, "text";\n',
  map: '',
  warnings: []
}
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.

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:

esbuild home.ts settings.ts --bundle --outdir=out
require('esbuild').buildSync({
  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:

esbuild out1=home.js out2=settings.js --bundle --outdir=out
require('esbuild').buildSync({
  entryPoints: {
    out1: 'home.js',
    out2: 'settings.js',
  },
  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.js",
    }, {
      OutputPath: "out2",
      InputPath:  "settings.js",
    }},
    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.

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:

echo 'require("fsevents")' > app.jsesbuild app.js --bundle --external:fsevents --platform=node// app.js
require("fsevents");
require('fs').writeFileSync('app.js', 'require("fsevents")')require('esbuild').buildSync({
  entryPoints: ['app.js'],
  outfile: 'out.js',
  bundle: true,
  platform: 'node',
  external: ['fsevents'],
}){ errors: [], warnings: [] }
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/. When a * wildcard character is present in an external path, that pattern will be applied to the original path in the source code instead of to the path after it has been resolved to a real file system path. This lets you match on paths that aren't real file system paths.

Format

Supported by: Transform | Build

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:

echo 'alert("test")' | esbuild --format=iife(() => {
  alert("test");
})();
let js = 'alert("test")'
let out = require('esbuild').transformSync(js, {
  format: 'iife',
})
process.stdout.write(out.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:

echo 'export default "test"' | esbuild --format=cjs...
__export(exports, {
  default: () => stdin_default
});
var stdin_default = "test";
let js = 'export default "test"'
let out = require('esbuild').transformSync(js, {
  format: 'cjs',
})
process.stdout.write(out.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:

echo 'module.exports = "test"' | esbuild --format=esm...
var require_stdin = __commonJS({
  "<stdin>"(exports, module) {
    module.exports = "test";
  }
});
export default require_stdin();
let js = 'module.exports = "test"'
let out = require('esbuild').transformSync(js, {
  format: 'esm',
})
process.stdout.write(out.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:

  • In the browser, you can load a module using <script src="file.js" type="module"></script>.
  • In node, you can load a module using node --experimental-modules file.mjs. Note that node requires the .mjs extension unless you have configured "type": "module" in your package.json file. You can use the out extension setting in esbuild to customize the output extension for the files esbuild generates. You can read more about using ECMAScript modules in node here.

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-shim.js that exports a variable named process:

// process-shim.js
export let 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 uses of the global identifier process with an import to that file:

esbuild entry.js --bundle --inject:./process-shim.js --outfile=out.js
require('esbuild').buildSync({
  entryPoints: ['entry.js'],
  bundle: true,
  inject: ['./process-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"},
    Bundle:      true,
    Inject:      []string{"./process-shim.js"},
    Outfile:     "out.js",
    Write:       true,
  })

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

That results in something like this:

// out.js
let process = {cwd: () => ""};
console.log(process.cwd());

Using inject with define

You can also combine this with the define feature to be more selective about what you import. For example:

// process-shim.js
export function dummy_process_cwd() {
  return ''
}
// entry.js
console.log(process.cwd())

You can map process.cwd to dummy_process_cwd with the define feature, then inject dummy_process_cwd from process-shim.js with the inject feature:

esbuild entry.js --bundle --define:process.cwd=dummy_process_cwd --inject:./process-shim.js --outfile=out.js
require('esbuild').buildSync({
  entryPoints: ['entry.js'],
  bundle: true,
  define: { 'process.cwd': 'dummy_process_cwd' },
  inject: ['./process-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"},
    Bundle:      true,
    Define: map[string]string{
      "process.cwd": "dummy_process_cwd",
    },
    Inject:  []string{"./process-shim.js"},
    Outfile: "out.js",
    Write:   true,
  })

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

That results in the following output:

// out.js
function dummy_process_cwd() {
  return "";
}
console.log(dummy_process_cwd());

Auto-import for JSX

You can use the inject feature to automatically provide the implementation for JSX expressions. For example, you can auto-import the react package to provide functions such as React.createElement. See the JSX documentation for details.

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.

Loader

Supported by: Transform | Build

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:

esbuild app.js --bundle --loader:.png=dataurl --loader:.svg=text
require('esbuild').buildSync({
  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:

echo 'import pkg = require("./pkg")' | esbuild --loader=ts --bundle
require('esbuild').buildSync({
  stdin: {
    contents: 'import pkg = require("./pkg")',
    loader: 'ts',
    resolveDir: __dirname,
  },
  bundle: true,
  outfile: 'out.js',
})
package main

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

func main() {
  cwd, err := os.Getwd()
  if err != nil {
    log.Fatal(err)
  }

  result := api.Build(api.BuildOptions{
    Stdin: &api.StdinOptions{
      Contents:   "import pkg = require('./pkg')",
      Loader:     api.LoaderTS,
      ResolveDir: cwd,
    },
    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:

echo 'let x: number = 1' | esbuild --loader=tslet x = 1;
let ts = 'let x: number = 1'require('esbuild').transformSync(ts, {
  loader: 'ts',
}){
  code: 'let x = 1;\n',
  map: '',
  warnings: []
}
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)
  }
}

Minify

Supported by: Transform | Build

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:

echo 'fn = obj => { return obj.x }' | esbuild --minifyfn=n=>n.x;
var js = 'fn = obj => { return obj.x }'require('esbuild').transformSync(js, {
  minify: true,
}){
  code: 'fn=n=>n.x;\n',
  map: '',
  warnings: []
}
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:

echo 'fn = obj => { return obj.x }' | esbuild --minify-whitespacefn=obj=>{return obj.x};echo 'fn = obj => { return obj.x }' | esbuild --minify-identifiersfn = (n) => {
  return n.x;
};echo 'fn = obj => { return obj.x }' | esbuild --minify-syntaxfn = (obj) => obj.x;
var js = 'fn = obj => { return obj.x }'require('esbuild').transformSync(js, {
  minifyWhitespace: true,
}){
  code: 'fn=obj=>{return obj.x};\n',
  map: '',
  warnings: []
}require('esbuild').transformSync(js, {
  minifyIdentifiers: true,
}){
  code: 'fn = (n) => {\n  return n.x;\n};\n',
  map: '',
  warnings: []
}require('esbuild').transformSync(js, {
  minifySyntax: true,
}){
  code: 'fn = (obj) => obj.x;\n',
  map: '',
  warnings: []
}
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:

echo 'div { color: yellow }' | esbuild --loader=css --minifydiv{color:#ff0}
var css = 'div { color: yellow }'require('esbuild').transformSync(css, {
  loader: 'css',
  minify: true,
}){
  code: 'div{color:#ff0}\n',
  map: '',
  warnings: []
}
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:

  • You should probably also set the target option when minification is enabled. By default esbuild takes advantage of modern JavaScript features to make your code smaller. For example, a === undefined || a === null ? 1 : a could be minified to a ?? 1. If you do not want esbuild to take advantage of modern JavaScript features when minifying, you should use an older language target such as --target=es6.

  • Minification is not safe for 100% of all JavaScript code. This is true for esbuild as well as for other popular JavaScript minifiers such as terser. In particular, esbuild is not designed to preserve the value of calling .toString() on a function. The reason for this is because if all code inside all functions had to be preserved verbatim, minification would hardly do anything at all and would be virtually useless. However, this means that JavaScript code relying on the return value of .toString() will likely break when minified. For example, some patterns in the AngularJS framework break when code is minified because AngularJS uses .toString() to read the argument names of functions. A workaround is to use explicit annotations instead.

  • By default esbuild does not preserve the value of .name on function and class objects. This is because most code doesn't rely on this property and using shorter names is an important size optimization. However, some code does rely on the .name property for registration and binding purposes. If you need to rely on this you should enable the keep names option.

  • Use of certain JavaScript features can disable many of esbuild's optimizations including minification. Specifically, using direct eval and/or the with statement prevent esbuild from renaming identifiers to smaller names since these features cause identifier binding to happen at run time instead of compile time. This is almost always unintentional, and only happens because people are unaware of what direct eval is and why it's bad.

    If you are thinking about writing some code like this:

    // Direct eval (will disable minification for the whole file)
    let result = eval(something)
    

    You should probably write your code like this instead so your code can be minified:

    // Indirect eval (has no effect on the surrounding code)
    let result = (0, eval)(something)
    

    There is more information about the consequences of direct eval and the available alternatives here.

  • The minification algorithm in esbuild does not yet do advanced code optimizations. In particular, the following code optimizations are possible for JavaScript code but are not done by esbuild (not an exhaustive list):

    • Dead-code elimination within function bodies
    • Function inlining
    • Cross-statement constant propagation
    • Object shape modeling
    • Allocation sinking
    • Method devirtualization
    • Symbolic execution
    • JSX expression hoisting
    • TypeScript enum detection and inlining

    If your code makes use of patterns that require some of these forms of code optimization to be compact, or if you are searching for the optimal JavaScript minification algorithm for your use case, you should consider using other tools. Some examples of tools that implement some of these advanced code optimizations include Terser and Google Closure Compiler.

Outdir

Supported by: Build

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

esbuild app.js --bundle --outdir=out
require('esbuild').buildSync({
  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",
  })

  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:

esbuild app.js --bundle --outfile=out.js
require('esbuild').buildSync({
  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,
    Outdir:      "out.js",
  })

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

Platform

Supported by: Build

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:

esbuild app.js --bundle --platform=node
require('esbuild').buildSync({
  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 bundling is enabled the default output format is set to iife, which wraps the generated JavaScript code in an immediately-invoked function expression to prevent variables from leaking into the global scope.

  • If a package specifies a map for the browser field in its package.json file, esbuild will use that map to replace specific files or modules with their browser-friendly versions. For example, a package might contain a substitution of path with path-browserify.

  • The main fields setting is set to browser,module,main but with some additional special behavior. If a package supports module and main but not browser then main is used instead of module if that package is ever imported using require(). This behavior improves compatibility with CommonJS modules that export a function by assigning it to module.exports.

  • The conditions setting automatically includes the browser condition. This changes how the exports field in package.json files is interpreted to prefer browser-specific code.

  • When using the build API, all process.env.NODE_ENV expressions are automatically defined to "production" if all minification options are enabled and "development" otherwise. This only happens if process, process.env, and process.env.NODE_ENV are not already defined. This substitution is necessary to avoid React-based code crashing instantly (since process is a node API, not a web API).

When the platform is set to node:

  • When bundling is enabled the default output format is set to cjs, which stands for CommonJS (the module format used by node). ES6-style exports using export statements will be converted into getters on the CommonJS exports object.

  • All built-in node modules such as fs are automatically marked as external so they don't cause errors when the bundler tries to bundle them.

  • The main fields setting is set to main,module. This means tree shaking will likely not happen for packages that provide both module and main since tree shaking works with ECMAScript modules but not with CommonJS modules.

    Unfortunately some packages incorrectly treat module as meaning "browser code" instead of "ECMAScript module code" so this default behavior is required for compatibility. You can manually configure the main fields setting to module,main if you want to enable tree shaking and know it is safe to do so.

  • The conditions setting automatically includes the node condition. This changes how the exports field in package.json files is interpreted to prefer node-specific code.

When the platform is set to neutral:

  • When bundling is enabled the default output format is set to esm, which uses the export syntax introduced with ECMAScript 2015 (i.e. ES6). You can change the output format if this default is not appropriate.

  • The main fields setting is empty by default. If you want to use npm-style packages, you will likely have to configure this to be something else such as main for the standard main field used by node.

  • The conditions setting does not automatically include any platform-specific values.

See also bundling for the browser and bundling for node.

Serve

Supported by: Build

During development, it's common to switch back and forth between a text editor and a browser while making changes. It's inconvenient to manually re-run esbuild before reloading your code in the browser. There are several methods to automate this:

  • Use watch mode to re-run esbuild when a file is changed
  • Configure your text editor to run esbuild every time you save
  • Serve your code with a web server that rebuilds on every request

This API call implements the last method. The serve API is similar to the build API call but instead of writing the generated files to the file system, it starts a long-lived local HTTP web server that serves the generated files from the latest build. Each new batch of requests causes esbuild to re-run the build command before responding to the requests so your files are always up to date.

The advantage of this method over the other methods is that the web server can delay the browser's request until the build has finished. That way reloading your code in the browser before the latest build has finished will never run code from a previous build. The files are served from memory and are not written to the file system to ensure that the outdated files cannot be observed.

Note that this is intended to only be used in development. Do not use this in production. In production you should be serving static files without using esbuild as a web server.

There are two different approaches for using the serve API:

Approach 1: Serve everything with esbuild

With this approach, you give esbuild a directory called servedir with extra content to serve in addition to the files that esbuild generates. This works well for simple situations where you are creating some static HTML pages and want to use esbuild to bundle the JavaScript and/or CSS. You can put your HTML files in the servedir and your other source code outside of the servedir, then set the outdir somewhere inside the servedir:

esbuild src/app.js --servedir=www --outdir=www/js --bundle
require('esbuild').serve({
  servedir: 'www',
}, {
  entryPoints: ['src/app.js'],
  outdir: 'www/js',
  bundle: true,
}).then(server => {
  // Call "stop" on the web server to stop serving
  server.stop()
})
server, err := api.Serve(api.ServeOptions{
  Servedir: "www",
}, api.BuildOptions{
  EntryPoints: []string{"src/app.js"},
  Outdir:      "www/js",
  Bundle:      true,
})

// Call "stop" on the web server to stop serving
server.Stop()

In the above example, your www/index.html page could reference the compiled code in src/app.js like this:

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

When you do this, every HTTP request will cause esbuild to rebuild your code and serve you the latest version. So js/app.js will always be up to date every time you reload the page. Note that although the generated code appears to be inside the outdir directory, it's never actually written to the file system with the serve API. Instead the paths for generated code shadow (i.e. takes precedence over) other paths inside the servedir and generated files are served directly from memory.

The benefit of doing things this way is that you can use the exact same HTML pages in development and production. In development you can run esbuild with --servedir= and esbuild will serve the generated output files directly. For production you can omit that flag and esbuild will write the generated files to the file system. In both cases you should be getting the exact same result in the browser with the exact same code in both development and production.

The port is automatically chosen by default as the first open port equal to or greater than 8000. The port number is returned from the API call (or printed to the terminal for the CLI) so you can know which URL to visit. The port can be set to something specific if necessary (described further down below).

Approach 2: Only serve generated files with esbuild

With this approach, you just tell esbuild to serve the contents of the outdir without giving it any additional content to serve. This works well for more complex development setups. For example, you might want to use NGINX as a reverse proxy to route different paths to separate backend services during development (e.g. /static/ to NGINX, /api/ to node, /js/ to esbuild, etc.). Using esbuild with this approach looks like this:

esbuild src/app.js --outfile=out.js --bundle --serve=8000
require('esbuild').serve({
  port: 8000,
}, {
  entryPoints: ['src/app.js'],
  bundle: true,
  outfile: 'out.js',
}).then(server => {
  // Call "stop" on the web server to stop serving
  server.stop()
})
server, err := api.Serve(api.ServeOptions{
  Port: 8000,
}, api.BuildOptions{
  EntryPoints: []string{"src/app.js"},
  Bundle:      true,
  Outfile:     "out.js",
})

// Call "stop" on the web server to stop serving
server.Stop()

The API call in the above example would serve the compiled contents of src/app.js at http://localhost:8000/out.js. Just like with the first approach, every HTTP request will cause esbuild to rebuild your code and serve you the latest version so out.js will always be up to date. Your HTML file (served by another web server on another port) could then reference the compiled file from your HTML like this:

<script src="http://localhost:8000/out.js"></script>

The URL structure of the web server exactly mirrors the URL structure of the output directory when using the normal build command without the web server enabled. For example, if the output directory normally contains a file called ./pages/about.js, the web server will have a corresponding /pages/about.js path.

If you would like to browse the web server to see what URLs are available, you can use the built-in directory listing by visiting a directory name instead of a file name. For example, if you're running esbuild's web server on port 8000 you can visit http://localhost:8000/ in your browser to view the web server's root directory. From there you can click on links to browse to different files and directories on the web server.

Arguments

Notice that the serve API is a different API call than the build API. This is because starting a long-running web server is different enough to warrant different arguments and return values. The first argument to the serve API call is an options object with serve-specific options:

interface ServeOptions {
  port?: number;
  host?: string;
  servedir?: 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
  OnRequest func(ServeOnRequestArgs)
}

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

    The HTTP port can optionally be configured here. If omitted, it will default to an open port with a preference for port 8000. You can set the port on the command line by using --serve=8000 instead of just --serve.

  • host

    By default, esbuild makes the web server available on all IPv4 network interfaces. This corresponds to a host address of 0.0.0.0. If you would like to configure a different host (for example, to only serve on the 127.0.0.1 loopback interface without exposing anything to the network), you can specify the host using this argument. You can set the host on the command line by using --serve=127.0.0.1:8000 instead of just --serve.

    If you need to use IPv6 instead of IPv4, you just need to specify an IPv6 host address. The equivalent to the 127.0.0.1 loopback interface in IPv6 is ::1 and the equivalent to the 0.0.0.0 universal interface in IPv6 is ::. If you are setting the host to an IPv6 address on the command line, you need to surround the IPv6 address with square brackets to distinguish the colons in the address from the colon separating the host and port like this: --serve=[::]:8000.

  • servedir

    This is a directory of extra content for esbuild's HTTP server to serve instead of a 404 when incoming requests don't match any of the generated output file paths. This lets you use esbuild as a general-purpose local web server. For example, using esbuild --servedir=. serves the current directory on localhost. Using servedir is described in more detail above in the previous section about different approaches.

  • onRequest

    This is called once for each incoming request with some information about the request. This callback is used by the CLI to print out a log message for each request. The time field is the time to generate the data for the request, but it does not include the time to stream the request to the client.

    Note that this is called after the request has completed. It's not possible to use this callback to modify the request in any way. If you want to do this, you should put a proxy in front of esbuild instead.

The second argument to the serve API call is the normal set of options for the underlying build API that is called on every request. See the documentation for the build API for more information about these options.

Return values

interface ServeResult {
  port: number;
  host: string;
  wait: Promise<void>;
  stop: () => void;
}
type ServeResult struct {
  Port uint16
  Host string
  Wait func() error
  Stop func()
}
  • port

    This is the port that ended up being used by the web server. You'll want to use this if you don't specify a port since esbuild will end up picking an arbitrary open port, and you need to know which port it picked to be able to connect to it. If you're using the CLI, this port number will be printed to stderr in the terminal.

  • host

    This is the host that ended up being used by the web server. It will be 0.0.0.0 (i.e. serving on all available network interfaces) unless a custom host was configured.

  • wait

    The serve API call returns immediately as long as the socket was able to be opened. The wait return value provides a way to be informed when the web server is terminated, either due to a network error or due to stop being called at some point in the future.

  • stop

    Call this callback to stop the web server, which you should do when you no longer need it to clean up resources. This will immediately terminate all open connections and wake up any code waiting on the wait return value.

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. It adds a custom 404 page instead of esbuild's default 404 page:

const esbuild = require('esbuild');
const http = require('http');

// Start esbuild's server on a random local port
esbuild.serve({
  servedir: __dirname,
}, {
  // ... your build options go here ...
}).then(result => {
  // The result tells us where esbuild's local server is
  const {host, port} = result

  // 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:

  • Injecting your own 404 page (the example above)
  • Customizing the mapping of routes to files on the file system
  • Redirecting some routes to an API server instead of to esbuild
  • Adding support for HTTPS using your own self-signed certificates

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

Sourcemap

Supported by: Transform | Build

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:

esbuild app.ts --sourcemap --outfile=out.js
require('esbuild').buildSync({
  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:

esbuild app.ts --sourcemap=external --outfile=out.js
require('esbuild').buildSync({
  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:

esbuild app.ts --sourcemap=inline --outfile=out.js
require('esbuild').buildSync({
  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:

esbuild app.ts --sourcemap=both --outfile=out.js
require('esbuild').buildSync({
  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.

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:

  • Chrome: ⚙ → Enable JavaScript source maps
  • Safari: ⚙ → Sources → Enable source maps
  • Firefox: ··· → Enable Source Maps

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

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:

  • Code shared between multiple entry points is split off into a separate shared file that both entry points import. That way if the user first browses to one page and then to another page, they don't have to download all of the JavaScript for the second page from scratch if the shared part has already been downloaded and cached by their browser.

  • Code referenced through an asynchronous import() expression will be split off into a separate file and only loaded when that expression is evaluated. This allows you to improve the initial download time of your app by only downloading the code you need at startup, and then lazily downloading additional code if needed later.

    Without code splitting enabled, an import() expression becomes Promise.resolve().then(() => require()) instead. This still preserves the asynchronous semantics of the expression but it means the imported code is included in the same bundle instead of being split off into a separate file.

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

esbuild home.ts about.ts --bundle --splitting --outdir=out --format=esm
require('esbuild').buildSync({
  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)
  }
}

Target

Supported by: Transform | Build

This sets the target environment for the generated JavaScript and/or CSS code. For example, you can configure esbuild to not generate any newer JavaScript or CSS that Chrome version 58 can't handle. The target can either be set to a JavaScript language version such as es2020 or to a list of versions of individual engines (currently either chrome, firefox, safari, edge, or node). 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 uses all of the available target environment names in esbuild. Note that 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):

esbuild app.js --target=es2020,chrome58,firefox57,safari11,edge16,node12
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  target: [
    'es2020',
    'chrome58',
    'firefox57',
    'safari11',
    'edge16',
    'node12',
  ],
  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.EngineFirefox, Version: "57"},
      {Name: api.EngineSafari, Version: "11"},
      {Name: api.EngineEdge, Version: "16"},
      {Name: api.EngineNode, Version: "12"},
    },
    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.

Note that 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.

Watch

Supported by: Build

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

esbuild app.js --outfile=out.js --bundle --watch[watch] build finished, watching for changes...
require('esbuild').build({
  entryPoints: ['app.js'],
  outfile: 'out.js',
  bundle: true,
  watch: true,
}).then(result => {
  console.log('watching...')
})
result := api.Build(api.BuildOptions{
  EntryPoints: []string{"app.js"},
  Outfile:     "out.js",
  Bundle:      true,
  Watch:       &api.WatchMode{},
})
fmt.Printf("watching...\n")

If you are using the JavaScript or Go API, you can optionally provide a callback that will be called whenever an incremental build has completed. This can be used to do something once the build is complete (e.g. to reload your app in the browser):

require('esbuild').build({
  entryPoints: ['app.js'],
  outfile: 'out.js',
  bundle: true,
  watch: {
    onRebuild(error, result) {
      if (error) console.error('watch build failed:', error)
      else console.log('watch build succeeded:', result)
    },
  },
}).then(result => {
  console.log('watching...')
})
result := api.Build(api.BuildOptions{
  EntryPoints: []string{"app.js"},
  Outfile:     "out.js",
  Bundle:      true,
  Watch: &api.WatchMode{
    OnRebuild: func(result api.BuildResult) {
      if len(result.Errors) > 0 {
        fmt.Printf("watch build failed: %d errors\n", len(result.Errors))
      } else {
        fmt.Printf("watch build succeeded: %d warnings\n", len(result.Warnings))
      }
    },
  },
})
fmt.Printf("watching...\n")

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

require('esbuild').build({
  entryPoints: ['app.js'],
  outfile: 'out.js',
  bundle: true,
  watch: true,
}).then(result => {
  console.log('watching...')

  setTimeout(() => {
    result.stop()
    console.log('stopped watching')
  }, 10 * 1000)
})
result := api.Build(api.BuildOptions{
  EntryPoints: []string{"app.js"},
  Outfile:     "out.js",
  Bundle:      true,
  Watch:       &api.WatchMode{},
})
fmt.Printf("watching...\n")

time.Sleep(10 * time.Second)
result.Stop()
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 incremental build API and a file watcher library of your choice if you don't want to use a polling-based approach.

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:

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

for (let out of result.outputFiles) {
  console.log(out.path, out.contents)
}
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\n", out.Path, out.Contents)
  }
}

Advanced options

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:

esbuild app.js --outdir=. --allow-overwrite
require('esbuild').buildSync({
  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)
  }
}

Analyze

Supported by: Build

Using the analyze feature generates an easy-to-read report about the contents of your bundle:

esbuild --bundle example.jsx --outfile=out.js --minify --analyze...

  out.js                                                                    27.4kb  100.0%
   ├ node_modules/react-dom/cjs/react-dom-server.browser.production.min.js  19.2kb   70.1%
   ├ node_modules/react/cjs/react.production.min.js                          5.9kb   21.5%
   ├ 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%
(async () => {
  let esbuild = require('esbuild')

  let result = await esbuild.build({
    entryPoints: ['example.jsx'],
    outfile: 'out.js',
    minify: true,
    metafile: true,
  })

  let text = await esbuild.analyzeMetafile(result.metafile)
  console.log(text)
})()
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)
  }

  text := api.AnalyzeMetafile(result.Metafile, api.AnalyzeMetafileOptions{})
  fmt.Printf("%s", text)
}

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:

esbuild --bundle example.jsx --outfile=out.js --minify --analyze=verbose...

  out.js ─────────────────────────────────────────────────────────────────── 27.4kb ─ 100.0%
   ├ node_modules/react-dom/cjs/react-dom-server.browser.production.min.js ─ 19.2kb ── 70.1%
   │  └ node_modules/react-dom/server.browser.js
   │     └ example.jsx
   ├ node_modules/react/cjs/react.production.min.js ───────────────────────── 5.9kb ── 21.5%
   │  └ 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
(async () => {
  let esbuild = require('esbuild')

  let result = await esbuild.build({
    entryPoints: ['example.jsx'],
    outfile: 'out.js',
    minify: true,
    metafile: true,
  })

  let text = await esbuild.analyzeMetafile(result.metafile, {
    verbose: true,
  })
  console.log(text)
})()
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)
  }

  text := api.AnalyzeMetafile(result.Metafile, api.AnalyzeMetafileOptions{
    Verbose: true,
  })
  fmt.Printf("%s", text)
}

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.

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:

esbuild app.js --asset-names=assets/[name]-[hash] --loader:.png=file --bundle --outdir=out
require('esbuild').buildSync({
  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 three placeholders that can be used in asset path templates:

  • [dir]

    This is the relative path from the directory containing the asset file to the outbase directory. Its purpose is to help asset output paths look more aesthetically pleasing by mirroring the input directory structure inside of the output directory.

  • [name]

    This is the original file name of the asset without the extension. For example, if the asset was originally named image.png then [name] will be substituted with image in the template. It is not necessary to use this placeholder; it only exists to provide human-friendly asset names to make debugging easier.

  • [hash]

    This is the content hash of the asset, which is useful to avoid name collisions. For example, your code may import components/button/icon.png and components/select/icon.png in which case you'll need the hash to distinguish between the two assets that are both named icon.

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.

Supported by: Transform | Build

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

esbuild app.js --banner:js=//comment --banner:css=/*comment*/
require('esbuild').buildSync({
  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: Transform | Build

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:

echo 'let π = Math.PI' | esbuildlet \u03C0 = Math.PI;echo 'let π = Math.PI' | esbuild --charset=utf8let π = Math.PI;
let js = 'let π = Math.PI'require('esbuild').transformSync(js){
  code: 'let \\u03C0 = Math.PI;\n',
  map: '',
  warnings: []
}require('esbuild').transformSync(js, {
  charset: 'utf8',
}){
  code: 'let π = Math.PI;\n',
  map: '',
  warnings: []
}
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:

  • This does not yet escape non-ASCII characters embedded in regular expressions. This is because esbuild does not currently parse the contents of regular expressions at all. The flag was added despite this limitation because it's still useful for code that doesn't contain cases like this.

  • This flag does not apply to comments. I believe preserving non-ASCII data in comments should be fine because even if the encoding is wrong, the run time environment should completely ignore the contents of all comments. For example, the V8 blog post mentions an optimization that avoids decoding comment contents completely. And all comments other than license-related comments are stripped out by esbuild anyway.

  • This option simultaneously applies to all output file types (JavaScript, CSS, and JSON). So if you configure your web server to send the correct Content-Type header and want to use the UTF-8 charset, make sure your web server is configured to treat both .js and .css files as UTF-8.

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:

esbuild app.js --chunk-names=chunks/[name]-[hash] --bundle --outdir=out --splitting --format=esm
require('esbuild').buildSync({
  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 two placeholders that can be used in chunk path templates:

  • [name]

    This will currently always be the text chunk, although this placeholder may take on additional values in future releases.

  • [hash]

    This is the content hash of the chunk. Including this is necessary to distinguish different chunks from each other in the case where multiple chunks of shared code are generated.

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.

Color

Supported by: Transform | Build

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"

    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.

1 warning and 1 error

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:

echo 'typeof x == "null"' | esbuild --color=true 2> stderr.txt
let js = 'typeof x == "null"'
require('esbuild').transformSync(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.

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:

esbuild src/app.js --bundle --conditions=custom1,custom2
require('esbuild').buildSync({
  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:

  • default

    This condition is always active. It is intended to come last and lets you provide a fallback for when no other condition applies.

  • import

    This condition is only active when the import path is from an ESM import statement or import() expression. It can be used to provide ESM-specific code.

  • require

    This condition is only active when the import path is from a CommonJS require() call. It can be used to provide CommonJS-specific code.

  • browser

    This condition is only active when esbuild's platform setting is set to browser. It can be used to provide browser-specific code.

  • node

    This condition is only active when esbuild's platform setting is set to node. It can be used to provide node-specific code.

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. The primary way of avoiding this is to put all of your code in the require condition and have the import condition just be a light wrapper that calls require on your package and re-exports the package using ESM syntax.

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:

esbuild src/main-app/app.js --entry-names=[dir]/[name]-[hash] --outbase=src --bundle --outdir=out
require('esbuild').buildSync({
  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 three placeholders that can be used in entry path templates:

  • [dir]

    This is the relative path from the directory containing the input entry point file to the outbase directory. Its purpose is to help you avoid collisions between identically-named entry points in different subdirectories.

    For example, if there are two entry points src/pages/home/index.ts and src/pages/about/index.ts, the outbase directory is src, and the entry names template is [dir]/[name], the output directory will contain pages/home/index.js and pages/about/index.js. If the entry names template had been just [name] instead, bundling would have failed because there would have been two output files with the same output path index.js inside the output directory.

  • [name]

    This is the original file name of the entry point without the extension. For example, if the input entry point file is named app.js then [name] will be substituted with app in the template.

  • [hash]

    This is the content hash of the output file, which can be used to take optimal advantage of browser caching. Adding [hash] to your entry point names means esbuild will calculate a hash that relates to all content in the corresponding output file (and any output file it imports if code splitting is active). The hash is designed to change if and only if any of the input files relevant to that output file are changed.

    After that, you can have your web server tell browsers that to cache these files forever (in practice you can say they expire a very long time from now such as in a year). You can then use the information in the metafile to determine which output file path corresponds to which input entry point so you know what path to include in your <script> tag.

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.

Supported by: Transform | Build

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

esbuild app.js --footer:js=//comment --footer:css=/*comment*/
require('esbuild').buildSync({
  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.

Global name

Supported by: Transform | Build

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:

echo 'module.exports = "test"' | esbuild --format=iife --global-name=xyz
let js = 'module.exports = "test"'
require('esbuild').transformSync(js, {
  format: 'iife',
  globalName: 'xyz',
})
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:

echo 'module.exports = "test"' | esbuild --format=iife --global-name='example.versions["1.0"]'
let js = 'module.exports = "test"'
require('esbuild').transformSync(js, {
  format: 'iife',
  globalName: 'example.versions["1.0"]',
})
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();
})();

Ignore annotations

Supported by: Transform | Build

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:

  • Inline /* @__PURE__ */ comments before function calls tell esbuild that the function call can be removed if the resulting value isn't used. See the pure API option for more information.

  • The sideEffects field in package.json can be used to tell esbuild which files in your package can be removed if all imports from that file end up being unused. This is a convention from Webpack and many libraries published to npm already have this field in their package definition. You can learn more about this field in Webpack's documentation for this field.

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:

esbuild app.js --bundle --ignore-annotations
require('esbuild').buildSync({
  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.

Incremental

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 a file watcher service. Incremental builds are more efficient than regular builds because some of the data is cached and can be reused if the original files haven't changed since the last build. There are currently two forms of caching used by the incremental build API:

  • Files are stored in memory and are not re-read from the file system if the file metadata hasn't changed since the last build. This optimization only applies to file system paths. It does not apply to virtual modules created by plugins.

  • Parsed ASTs are stored in memory and re-parsing the AST is avoided if the file contents haven't changed since the last build. This optimization applies to virtual modules created by plugins in addition to file system modules, as long as the virtual module path remains the same.

Here's how to do an incremental build:

async function example() {
  let result = await require('esbuild').build({
    entryPoints: ['app.js'],
    bundle: true,
    outfile: 'out.js',
    incremental: true,
  })

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

  // Call "dispose" when you're done to free up resources.
  result.rebuild.dispose()
}

example()
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",
    Incremental: true,
  })
  if len(result.Errors) > 0 {
    os.Exit(1)
  }

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

JSX

Supported by: Transform | Build

This option tells esbuild what to do about JSX syntax. You can either have esbuild transform JSX to JS (the default) or preserve the JSX syntax in the output. To preserve JSX syntax:

echo '<div/>' | esbuild --jsx=preserve --loader=jsx<div />;
require('esbuild').transformSync('<div/>', {
  jsx: 'preserve',
  loader: 'jsx',
}){
  code: '<div />;\n',
  map: '',
  warnings: []
}
package main

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

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

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

Note that if you preserve JSX syntax, the output files are no longer valid JavaScript code. This feature is intended to be used when you want to transform the JSX syntax in esbuild's output files by another tool after bundling, usually one with a different JSX-to-JS transform than the one esbuild implements.

JSX factory

Supported by: Transform | Build

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):

echo '<div/>' | esbuild --jsx-factory=h --loader=jsx/* @__PURE__ */ h("div", null);
require('esbuild').transformSync('<div/>', {
  jsxFactory: 'h',
  loader: 'jsx',
}){
  code: '/* @__PURE__ */ h("div", null);\n',
  map: '',
  warnings: []
}
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"
  }
}

JSX fragment

Supported by: Transform | Build

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):

echo '<>x</>' | esbuild --jsx-fragment=Fragment --loader=jsx/* @__PURE__ */ React.createElement(Fragment, null, "x");
require('esbuild').transformSync('<>x</>', {
  jsxFragment: 'Fragment',
  loader: 'jsx',
}){
  code: '/* @__PURE__ */ React.createElement(Fragment, null, "x");\n',
  map: '',
  warnings: []
}
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"
  }
}

Keep names

Supported by: Transform | Build

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:

esbuild app.js --minify --keep-names
require('esbuild').buildSync({
  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)
  }
}

Supported by: Transform | Build

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:

  • none
    Do not preserve any legal comments.

  • inline
    Preserve all legal comments.

  • eof
    Move all legal comments to the end of the file.

  • linked
    Move all legal comments to a .LEGAL.txt file and link to them with a comment.

  • external
    Move all legal comments to a .LEGAL.txt file but to not link to them.

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

esbuild app.js --legal-comments=eof
require('esbuild').buildSync({
  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 license comments.

Log level

Supported by: Transform | Build

The log level can be changed to prevent esbuild from printing warning and/or error messages to the terminal. The six log levels are:

  • silent
    Do not show any log output. This is the default log level when using the JS transform API.

  • error
    Only show errors.

  • warning
    Only show warnings and errors. This is the default log level when using the JS build API.

  • info
    Show warnings, errors, and an output file summary. This is the default log level when using the CLI.

  • debug
    Log everything from info and some additional messages that may help you debug a broken bundle. This log level has a performance impact and some of the messages may be false positives, so this information is not shown by default.

  • verbose
    This generates a torrent of log messages and was added to debug issues with file system drivers. It's not intended for general use.

The log level can be set like this:

echo 'typeof x == "null"' | esbuild --log-level=error
let js = 'typeof x == "null"'
require('esbuild').transformSync(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: Transform | Build

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:

esbuild app.js --log-limit=0
require('esbuild').buildSync({
  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)
  }
}

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:

  • main

    This is the standard field for all packages that are meant to be used with node. The name main is hard-coded in to node's module resolution logic itself. Because it's intended for use with node, it's reasonable to expect that the file path in this field is a CommonJS-style module.

  • module

    This field came from a proposal for how to integrate ECMAScript modules into node. Because of this, it's reasonable to expect that the file path in this field is an ECMAScript-style module. This proposal wasn't adopted by node (node uses "type": "module" instead) but it was adopted by major bundlers because ECMAScript-style modules lead to better tree shaking, or dead code removal.

    For package authors: Some packages incorrectly use the module field for browser-specific code, leaving node-specific code for the main field. This is probably because node ignores the module field and people typically only use bundlers for browser-specific code. However, bundling node-specific code is valuable too (e.g. it decreases download and boot time) and packages that put browser-specific code in module prevent bundlers from being able to do tree shaking effectively. If you are trying to publish browser-specific code in a package, use the browser field instead.

  • browser

    This field came from a proposal that allows bundlers to replace node-specific files or modules with their browser-friendly versions. It lets you specify an alternate browser-specific entry point. Note that it is possible for a package to use both the browser and module field together (see the note below).

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

esbuild app.js --bundle --main-fields=module,main
require('esbuild').buildSync({
  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 to fill out all four entries in the full CommonJS-vs-ESM and browser-vs-node compatibility matrix, you want 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"
  }
}

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:

esbuild app.js --bundle --metafile=meta.json --outfile=out.js
const result = require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  metafile: true,
  outfile: 'out.js',
})
require('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 example, bundle buddy can consume esbuild's metadata format and generates a treemap visualization of the modules in your bundle and how much space each one takes up.

The metadata JSON format looks like this (described using a TypeScript interface):

interface Metadata {
  inputs: {
    [path: string]: {
      bytes: number
      imports: {
        path: string
        kind: string
      }[]
    }
  }
  outputs: {
    [path: string]: {
      bytes: number
      inputs: {
        [path: string]: {
          bytesInOutput: number
        }
      }
      imports: {
        path: string
        kind: string
      }[]
      exports: string[]
      entryPoint?: string
    }
  }
}

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:

NODE_PATH=someDir esbuild app.js --bundle --outfile=out.js
require('esbuild').buildSync({
  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.

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:

esbuild app.js --bundle --outdir=dist --out-extension:.js=.mjs
require('esbuild').buildSync({
  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",
    OutExtensions: 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:

esbuild src/pages/home/index.ts src/pages/about/index.ts --bundle --outdir=out --outbase=src
require('esbuild').buildSync({
  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.

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:

esbuild app.js --bundle --preserve-symlinks --outfile=out.js
require('esbuild').buildSync({
  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.

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:

esbuild app.js --bundle --loader:.png=file --public-path=https://www.example.com/v1 --outdir=out
require('esbuild').buildSync({
  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)
  }
}

Pure

Supported by: Transform | Build

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 console.log 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:

echo 'console.log("foo:", foo())' | esbuild --pure:console.log/* @__PURE__ */ console.log("foo:", foo());echo 'console.log("foo:", foo())' | esbuild --pure:console.log --minifyfoo();
let js = 'console.log("foo:", foo())'require('esbuild').transformSync(js, {
  pure: ['console.log'],
}){
  code: '/* @__PURE__ */ console.log("foo:", foo());\n',
  map: '',
  warnings: []
}require('esbuild').transformSync(js, {
  pure: ['console.log'],
  minify: true,
}){
  code: 'foo();\n',
  map: '',
  warnings: []
}
package main

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

func main() {
  js := "console.log('foo:', foo())"

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

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

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

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

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:

esbuild app.js --bundle --resolve-extensions=.ts,.js
require('esbuild').buildSync({
  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)
  }
}

Source Root

Supported by: Transform | Build

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:

esbuild app.js --sourcemap --source-root=https://raw.githubusercontent.com/some/repo/v1.2.3/
require('esbuild').buildSync({
  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: Transform | Build

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:

cat app.js | esbuild --sourcefile=example.js --sourcemap
let fs = require('fs')
let js = fs.readFileSync('app.js', 'utf8')

require('esbuild').transformSync(js, {
  sourcefile: 'example.js',
  sourcemap: 'inline',
})
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", result.Code)
  }
}

Sources Content

Supported by: Transform | Build

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:

esbuild --bundle app.js --sourcemap --sources-content=false
require('esbuild').buildSync({
  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)
  }
}

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:

echo 'export * from "./another-file"' | esbuild --bundle --sourcefile=imaginary-file.js --loader=ts --format=cjs
let result = require('esbuild').buildSync({
  stdin: {
    contents: `export * from "./another-file"`,

    // These are all optional:
    resolveDir: require('path').join(__dirname, '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)
  }
}

Tree shaking

Supported by: Transform | Build

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 libraries 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 libraries 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 option.

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:

esbuild app.js --tree-shaking=true
require('esbuild').buildSync({
  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:

esbuild app.js --tree-shaking=false
require('esbuild').buildSync({
  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)
  }
}

Note that tree shaking automatically takes into account user-specified side-effect annotations. If you are bundling code with annotations that have been authored incorrectly, you may need to ignore annotations to make sure the bundled code is correct.

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:

esbuild app.ts --bundle --tsconfig=custom-tsconfig.json
require('esbuild').buildSync({
  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: Transform

This option can be used to pass your tsconfig.json file to the transform API, which doesn't access the file system. Using it looks like this:

echo 'class Foo { foo }' | esbuild --loader=ts --tsconfig-raw='{"compilerOptions":{"useDefineForClassFields":true}}'
let ts = 'class Foo { foo }'
require('esbuild').transformSync(ts, {
  loader: 'ts',
  tsconfigRaw: `{
    "compilerOptions": {
      "useDefineForClassFields": true,
    },
  }`,
})
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": true,
      },
    }`,
  })

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

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 override it:

require('esbuild').buildSync({
  entryPoints: ['file.js'],
  absWorkingDir: process.cwd(),
  outfile: 'out.js',
})
package main

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

func main() {
  cwd, err := os.Getwd()
  if err != nil {
    log.Fatal(err)
  }

  result := api.Build(api.BuildOptions{
    EntryPoints:   []string{"file.js"},
    AbsWorkingDir: cwd,
    Outfile:       "out.js",
  })

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

JS-specific details

The node-based JS API comes in both synchronous and asynchronous flavors, each with different tradeoffs. It's important to be aware of the differences to pick the correct one for your situation:

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:

  • Avoiding promises can result in cleaner code
  • Works in situations that must be synchronous such as within require.extensions

Cons:

  • You can't use plugins with the synchronous API since plugins are asynchronous
  • It blocks the current thread so you can't perform other work in the meantime
  • Using the synchronous API prevents esbuild from parallelizing esbuild API calls

Async API

Asynchronous API calls return their results using a promise:

let esbuild = require('esbuild')
esbuild.transform(code, options).then(result => { ... })
esbuild.build(options).then(result => { ... })

Pros:

  • You can use plugins with the asynchronous API
  • The current thread is not blocked so you can perform other work in the meantime
  • You can run many simultaneous esbuild API calls concurrently which are then spread across all available CPUs for maximum performance

Cons:

  • Using promises can result in messier code, especially in CommonJS where top-level await is not available
  • Doesn't work in situations that must be synchronous such as within require.extensions

Running 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:

let esbuild = require('esbuild-wasm')

esbuild.initialize({
  wasmURL: './node_modules/esbuild-wasm/esbuild.wasm',
}).then(() => {
  esbuild.transform(code, options).then(result => { ... })
  esbuild.build(options).then(result => { ... })
})

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 injecting the lib/browser.min.js file. 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 need 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'

  esbuild.initialize({
    wasmURL: './node_modules/esbuild-wasm/esbuild.wasm',
  }).then(() => { ... })
</script>

© 2020 Evan Wallace
Licensed under the MIT License.
https://esbuild.github.io/api/