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.

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

CLI JS Go
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:

CLI JS Go
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',
}){ 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:

CLI JS Go
esbuild in.js --bundle
require('esbuild').buildSync({
  entryPoints: ['in.js'],
  bundle: true,
  outfile: 'out.js',
}){ 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 concatentation. Passing esbuild multiple input files with bundling enabled will create two 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.

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

CLI JS Go
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:

CLI JS Go
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.

# External

Supported by: Build

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

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

CLI JS Go
echo 'require("fsevents")' > app.jsesbuild app.js --bundle --external:fsevents(() => {
  // app.js
  require("fsevents");
})();
require('fs').writeFileSync('app.js', 'require("fsevents")')require('esbuild').buildSync({
  entryPoints: ['app.js'],
  outfile: 'out.js',
  bundle: true,
  external: ['fsevents'],
}){ 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,
    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: iife, cjs, and esm.

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 is the default format unless you set platform to node. Using it looks like this:

CLI JS Go
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 is the default format when you set platform to node. Using it looks like this:

CLI JS Go
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. Using it looks like this:

CLI JS Go
echo 'module.exports = "test"' | esbuild --format=esm...
var require_stdin = __commonJS((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:

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

CLI JS Go
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:

CLI JS Go
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();
})();

# 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:.

CLI JS Go
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());

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

CLI JS Go
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.

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.

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

CLI JS Go
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):

CLI JS Go
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"
  }
}

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

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

CLI JS Go
esbuild app.js --bundle --loader:.png=dataurl
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  loader: { '.png': 'dataurl' },
  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,
    },
    Write: true,
  })

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

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

CLI JS Go
echo 'import pkg = require("./pkg")' | esbuild --loader=ts --bundle
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:

CLI JS Go
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:

CLI JS Go
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:

CLI JS Go
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:

CLI JS Go
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 in size to the minified output of industry-standard JavaScript minification tools. You can check out this benchmark for an example comparison of output sizes between tools. 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, and of course to do so much faster.

# Outdir

Supported by: Build

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

CLI JS Go
esbuild app.js --bundle --outdir=out
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 chould change the outbase directory.

# Outfile

Supported by: Build

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

CLI JS Go
esbuild app.js --bundle --outfile=out.js
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:

CLI JS Go
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 the platform is set to node:

When the platform is set to neutral:

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:

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. Here's how to use the serve API:

CLI JS Go
esbuild app.js --outfile=out.js --bundle --serve
require('esbuild').serve({}, {
  entryPoints: ['app.js'],
  bundle: true,
  outfile: 'out.js',
}).then(server => {
  // Call "stop" on the web server when you're done
  server.stop()
})
server, err := api.Serve(api.ServeOptions{}, api.BuildOptions{
  EntryPoints: []string{"app.js"},
  Bundle:      true,
  Outfile:     "out.js",
})

// Call "stop" on the web server when you're done
server.Stop()

This will start a local web server on an open port (8000 by default). You can then add something like this to your HTML to reference the generated output files. Every time you reload the page, app.js will automatically be rebuilt so it will always be up to date:

<script src="http://localhost:8000/app.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.

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.

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:

JS Go
interface ServeOptions {
  port?: number;
  host?: string;
  onRequest?: (args: ServeOnRequestArgs) => void;
}

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

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

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

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

# 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).

Enabling source map generation will generate a .js.map file alongside any generated .js file and add a special //# sourceMappingURL= comment to the bottom of the .js file pointing to the .js.map file:

CLI JS Go
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)
  }
}

If the input file itself contains a special //# sourceMappingURL= comment, esbuild will automatically try to parse the linked source map. If successful, the mappings in the generated source map will map all the way back to the original source code referenced in the input source map.

If you want to omit the special //# sourceMappingURL= comment from the generated .js file but you still want to generate the .js.map files, you should set the source map mode to external:

CLI JS Go
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)
  }
}

If you want to insert the entire source map into the .js file instead of generating a separate .js.map file, you should set the source map mode to inline:

CLI JS Go
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)
  }
}

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.

If you want to have the effect of both inline and external simultaneously, you should set the source map mode to both:

CLI JS Go
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)
  }
}

# Splitting

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

This enables "code splitting" which serves two purposes:

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

CLI JS Go
esbuild home.ts about.ts --bundle --splitting --outdir=out --format=esm
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 code. For example, you can configure esbuild to not generate any newer JavaScript that node version 10 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).

Here is an example with all possible target environments specified (you don't have to specify all of them):

CLI JS Go
esbuild app.js --target=es2020,chrome58,firefox57,safari11,edge16,node12.19.0
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  target: [
    'es2020',
    'chrome58',
    'firefox57',
    'safari11',
    'edge16',
    'node12.19.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"},
    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.19.0"},
    },
    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.

# Write

Supported by: Build

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

JS Go
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

Supported by: Transform | Build

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

CLI JS Go
esbuild app.js --banner='/* comment */'
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  banner: '/* 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:      "/* comment */",
  })

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

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

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

CLI JS Go
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:

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

 > example.js: error: Could not resolve "logger" (mark it as external to exclude it from the bundle)
    1 │ import log from "logger"
~~~~~~~~

 > example.js: warning: The "typeof" operator will never evaluate to "null"
    2 │ log(typeof x == "null")
~~~~~~

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:

CLI JS Go
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.

# Error limit

Supported by: Transform | Build

By default, esbuild stops reporting errors after 10 errors have been reached. This avoids the accidental generation of an overwhelming number of error 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 error limit can be changed to another value, and can also be disabled completely by setting it to zero. This will show all build errors:

CLI JS Go
esbuild app.js --error-limit=0
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  errorLimit: 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"},
    ErrorLimit:  0,
  })

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

Supported by: Transform | Build

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

CLI JS Go
esbuild app.js --footer='/* comment */'
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  footer: '/* 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:      "/* comment */",
  })

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

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

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

Here's how to do an incremental build:

JS Go
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)
    }
  }
}

# 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() {};
obj.fn = function() {};
fn = function() {};
let [fn = function() {}] = [];
let {fn = function() {}} = {};
[fn = function() {}] = [];
({fn = function() {}} = {});

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

CLI JS Go
esbuild app.js --minify --keep-names
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)
  }
}

# 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 four log levels are info (show everything), warning (show warnings and errors), error (just show errors), and silent (show nothing).

For example, you can hide all warnings by setting the log level to error:

CLI JS Go
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)
  }
}

# Main fields

Supported by: Build

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

The default main fields depend on the current platform setting 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:

CLI JS Go
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 write out a JSON file with metadata about the build. The following example puts the metadata in a file called meta.json:

CLI JS Go
esbuild app.js --bundle --metafile=meta.json --outfile=out.js
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  metafile: 'meta.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.js"},
    Bundle:      true,
    Metafile:    "meta.json",
    Outfile:     "out.js",
    Write:       true,
  })

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

If you are using TypeScript, the type declaration with the schema for the JSON metadata can be imported from the esbuild package:

import { Metadata } from 'esbuild'

const metafile: Metadata =
  JSON.parse(fs.readFileSync('meta.json', 'utf8'))

This is what the type declaration looks like:

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[]
    }
  }
}

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.

# Out extension

Supported by: Build

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

CLI JS Go
esbuild app.js --bundle --outdir=dist --out-extension:.js=.mjs
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:

CLI JS Go
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.

# Public path

Supported by: Build

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

CLI JS Go
esbuild app.js --bundle --loader:.png=file --public-path=https://www.example.com/v1 --outdir=out
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 annoated 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:

CLI JS Go
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,.mjs,.cjs,.js,.css,.json:

CLI JS Go
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)
  }
}

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

CLI JS Go
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:

CLI JS Go
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:

CLI JS Go
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. Note that tree shaking in esbuild is always enabled during bundling and can't be turned off, since trimming unused code makes the resulting file smaller without changing observable behavior.

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.

Manual annotations

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 unused. Currently there are two forms of tree-shaking annotations that esbuild supports:

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

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

CLI JS Go
esbuild app.js --bundle --tree-shaking=ignore-annotations
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  treeShaking: 'ignore-annotations',
  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,
    TreeShaking: api.TreeShakingIgnoreAnnotations,
  })

  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.

# Tsconfig

Supported by: Build

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

CLI JS Go
esbuild app.ts --bundle --tsconfig=custom-tsconfig.json
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:

CLI JS Go
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)
  }
}

# JS-specific details

Because JavaScript is single-threaded, there are several different ways to invoke the API with different performance and convenience tradeoffs. It's important to be aware of the differences to pick.

First there is the synchronous API. This is the most convenient option because single-threaded JavaScript code has the cleanest syntax. It's also optimal performance-wise if all you need to do is run esbuild and then exit. However, it blocks the main thread so you do not want to use it if you have other work to perform in the meantime. This is also the only option that cannot use plugins (since plugins are asynchronous). It looks like this:

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

Then there is the asynchronous API. Each call returns a promise instead of completing immediately. Under the hood, each call spawns a new child process using the esbuild binary and communicates with it over the stdin, stdout, and stderr pipes using a custom binary protocol. This is ideal if you only need to run esbuild once but you need to do other work in the background. Using it looks something like this:

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

However, if you need to run esbuild many times (for example, you want to transform lots of files) then you should use the service API for maximum performance. This keeps a long-running esbuild child process in the background and streams API calls to it, which are then processed in parallel. While API calls still have relatively high latency, together they have very high throughput. This is the least convenient option because you need to keep the service object around and remember to call service.stop() when you are done with it. Using it might look something like this:

let esbuild = require('esbuild')

async function example() {
  let service = await esbuild.startService()

  try {
    let promises = []
    for (let i = 0; i < 10; i++) {
      promises.push(service.transform(code, options))
      promises.push(service.build(options))
    }
    console.log(await Promise.all(promises))
  }

  finally {
    service.stop()
  }
}

# Running in the browser

The esbuild transform API can also run in the browser using WebAssembly in a Web Worker (the build API is currently not supported in the browser). 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 only the createService() function works and you need to pass the URL of the WebAssembly binary when creating the service. Assuming you are using a bundler, that would look something like this:

let esbuild = require('esbuild-wasm')

(async () => {
  let service = await esbuild.startService({
    wasmURL: 'node_modules/esbuild-wasm/esbuild.wasm',
  })

  try {
    let promises = []
    for (let i = 0; i < 10; i++) {
      promises.push(service.transform(code, options))
      promises.push(service.build(options))
    }
    console.log(await Promise.all(promises))
  }

  finally {
    service.stop()
  }
})()

If you're already running this code from a worker and don't want startService 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 startService.

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 with a startService property:

<script src="node_modules/esbuild-wasm/lib/browser.min.js"></script>
<script>
  esbuild.startService({
    wasmURL: 'node_modules/esbuild-wasm/esbuild.wasm',
  }).then(service => { ... })
</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 { startService } from 'node_modules/esbuild-wasm/esm/browser.min.js'
  startService({
    wasmURL: 'node_modules/esbuild-wasm/esbuild.wasm',
  }).then(service => { ... })
</script>