API

可以通过如下三种方式调用 API:在命令行中调用,在 JavaScript 中调用, 在 Go 中调用。 概念和参数在这三种方式中基本相同,因此本文档将三种方式的 API 文档合并显示, 不再按照不同的使用方式来拆分文档。

在 esbuild 的 API 中有两种主要的 API 调用:transformbuild。理解使用哪一个 API 对你而言十分重要, 因为他们的工作方式不同。

如果你用的是 JavaScript,请务必阅读下面的 JS 特殊细节 章节。 你也可以查看 esbuild 的 TypeScript 类型定义 作为参考,这会对你有帮助。如果正在使用 Go 语言,请务必阅读自动生成的 Go 文档

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

transform API 操作单个字符串,而不访问文件系统。 这使其能够比较理想地在没有文件系统的环境中使用(比如浏览器)或者作为另一个工具链的一部分。 以下是一个比较简单的 transform 示例:

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

如果没有提供输入的文件并且没有 --bundle 标志的话,命令行接口就会调用此 API。 在这个用例中,输入的字符串来自标准输入(stdin),并且输出字符串转到标准输出(stdout)。 transform API 可以使用以下配置项:

一般配置项:

高级配置:

JS 特殊细节:

Build API

调用 build API 操作文件系统中的一个或多个文件。 它允许文件互相引用并且打包在一起。 这里是一个简单的 build 用例:

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',
}){ 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)
  }
}

如果至少提供一个输入文件或者存在 --bundle 标志, 那么命令行接口会调用该 API。 请注意 esbuild 不会 默认打包。 你必须传递 --bundle 标志启用打包。 如果没有提供输入文件,则从标准化输入(stdin)读取单个输入文件。 build API 可以使用以下配置项:

一般配置项:

高级配置:

JS 特殊细节:

一般配置项

Bundle

Supported by: Build

打包一个文件意味着将任何导入的依赖项内联到文件中。 这个过程是递归的,因为依赖的依赖(等等)也将被内联。 默认情况下,esbuild 将 不会 打包输入的文件。 打包必须想这样显式启用:

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

点击 快速开始指引 查看打包实际代码的例子。

请注意打包与文件连接不同。在启用打包时向 esbuild 传递多个输入文件 将创建两个单独的 bundle 而不是将输入文件连接在一起。 为了使用 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

该特性提供了一种用常量表达式替换全局标识符的方法。 它可以在不改变代码本身的情况下改变某些构建之间代码的行为:

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

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

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

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

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

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

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

替换表达式必须是一个 JSON 对象(null、boolean、number、string、array 或者 object) 或者一个标识符。除了数组和对象之外,替换表达式是内联替换的,这意味着他们可以参与常数折叠。 数组与对象替换表达式会被存储在一个变量中,然后被标识符引用而不是内联替换, 这避免了替换重复复制一个值,但也意味着该值不能参与常数折叠。

如果你想用字符串字面值替换某些东西,记住,传递给esbuild的替换值本身必须包含引号。 省略引号意味着替换的值是一个标识符:

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

如果你在使用 CLI 工具,请记住,不同的 shell 对于如何转义双引号字符有不同的规则 (当替换的值为一个字符串时会非常必要)。使用 \" 反斜杠转义,因为它可以在 bash 以及 Windows 命令提示符中生效。其他在 bash 中有效的转义双引号的方法,比如用单引号括起来, 在Windows上不起作用,因为 Windows 命令提示符不会删除单引号。这和你在 package.json 的 npm script 中使用 CLI 工具是相关的,人们期望在所有平台上工作:

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

如果你仍然在不同的 shell 中遇到跨平台引号转义问题,你讲可能会选择使用 JavaScript API。 There you can use regular JavaScript syntax to eliminate 你可以使用常规的 JavaScript 语法来消除跨平台差异。

Entry points

Supported by: Build

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

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

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

CLI JS Go
esbuild home.ts settings.ts --bundle --outdir=out
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:

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

你可以标记一个文件或者包为外部(external),从而将其从你的打包结果中移除。 导入将被保留(对于 iife 以及 cjs 格式使用 require,对于 esm 格式使用 import),而不是被打包, 并将在运行时进行计算。

这里有几个用法。首先,它可以用于去除你的 bundle 中你知道将永远不会被执行的代码路径中的无用代码。 例如,一个 package 可以会包含值运行在 node 端的代码,但是你只会将其用在浏览器中。 它还可以用于在运行时从不能打包的包导入 node 中的代码。例如,fsevents 包含 esbuild 不支持的本地拓展, 像这样将某些内容标记为外部(external):

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

你也可以在外部(external)路径中使用 * 通配符标记所有符合该模式的为外部(external)。 例如,你可以使用 *.png 移除所有的 .png 文件或者使用 /images/* 移除所有路径以 /images/ 开头的路径。

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

Format

Supported by: Transform | Build

为生成的 JavaScript 文件设置输出格式。有三个可能的值:iifecjsesm

IIFE

iife 格式代表“立即调用函数表达式(immediately-invoked function expression)”并且在浏览器中运行。 将你的代码包裹在一个函数表达式中,以确保代码中的所有变量不会与全局作用域中的变量冲突。 如果你的入口起点有你想要暴露在浏览器全局环境中的导出,你可以使用 global name 设置 global iife 为默认格式,除非你设置 platformnode。 像这样使用它:

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

cjs 格式打包代表"CommonJS" 并且在 node 环境中运行。它假设环境包括 exportsrequiremodule。在 ECMAScript 模块语法中带有导出的入口点将被转换为一个模块, 每个导出名称的 “exports” 上都有一个 getter。当你设置 platformnode 时, cjs 为默认格式。像这样使用它:

CLI JS Go
echo 'export default "test"' | esbuild --format=cjs...
var stdin_exports = {};
__export(stdin_exports, {
  default: () => stdin_default
});
module.exports = __toCommonJS(stdin_exports);
var stdin_default = "test";
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

esm 格式代表 "ECMAScript module"。它假设环境支持 importexport 语法。 在 CommonJS 模块语法中带有导出的入口点将被转换为 module.exports 值的单个 default 导出。 像这样使用它:

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

esm 格式可以在浏览器或者 node 中使用。但是你必须显式地以模块加载它。 如果你从其他模块 import,那么这是自动进行的。否则:

Inject

Supported by: Build

这个配置项可以自动替换从另一个文件引入的全局变量。 当你为无法控制的代码适配新环境时是非常有用的。 例如,假定你有一个叫做 process-shim.js 的文件,该文件导出了 process 变量:

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

这尝试替换 node 的 process.cwd() 函数的使用,以阻止包在浏览器中运行它而导致崩溃。 你可以使用 inject 特性将一个 import 置于文件中以替换所有的全局标识符 process

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

结果是这样的:

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

inject 与 define 一起使用

你可以与 define 特性结合使用,这样你的导入会更具可选择性。例如:

// 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)
  }
}

结果如下:

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

自动导入 JSX

你可以使用 inject 特性以自动提供 JSX 表达式的执行环境。例如,你可以自动导入 react 包以执行诸如 React.createElement 的函数。查看 JSX 文档 获取更多。

不使用 import 注入文件

你可以对不包含 exports 的文件使用该特性。在这种情况下,注入的文件就像每个输入文件都包含 import "./file.js" 一样出现在输出的前面。 由于 ECMAScript 模块的工作方式,这个注入仍然是 “卫生的”,因为在不同文件中具有相同名称的符号会被重命名, 这样它们就不会相互冲突。

选择性注入文件

如果你仅想当导出被实际使用的情况下 有条件 的引入一个文件,你应该通过将其置于 package 中并且 在 package.json 中添加 "sideEffects": false 以标记被注入的文件没有副作用。 该设置为 Webpack 公约, esbuild 中该公约对所有的导入文件生效,而不仅仅是注入文件。

Loader

Supported by: Transform | Build

该配置项改变了输入文件解析的方式。例如, js loader 将文件解析为 JavaScript, css loader 将文件解析为 CSS。查看 content types 获取内置 loader 的完整列表。

配置一个给定文件类型的 loader 可以让你使用 import 声明或者 require 调用来加载该文件类型。 例如,使用 data URL loader 配置 .png 文件拓展名, 这意味着导入 .png 文件会给你一个包含该图像内容的数据 URL:

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)

以上代码可以使用 build API 调用进行打包,就像这样:

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

如果你在 标准化输入(stdin) 中使用 build API,该配置项就会变的不同, 因为标准化输入(stdin)没有文件拓展名。使用 build API 为标准化输入(stdin)配置一个 loader, 就像这样:

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

transform API 调用仅使用一个 loader,因为它不涉及与文件系统的交互, 因此不需要处理文件拓展名。为 transform API 配置一个 loader(在这里是 ts loader),就像这样:

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

启用该配置时,生成的代码会被压缩而不是格式化输出。 压缩后的代码与未压缩代码是相等的,但是会更小。这意味着下载更快但是更难调试。 一般情况下在生产环境而不是开发环境压缩代码。

在 esbuild 中这样启用压缩:

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

该配置项结合起来做三件独立的事情:移除空格、重写语法使其更体积更小、重命名变量为更短的名称。 一般情况下这三件事情你都想做,但是如果有必要的话,这些配置项可以单独启用:

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

这些概念同样适用于 CSS,而不仅仅是 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)
  }
}

esbuild 中的 JavaScript 压缩算法生成的输出通常与行业标准 JavaScript 压缩工具生成的输出大小相近。 benchmark 有一个不同压缩工具之间的样例对比。尽管 esbuild 不是所有场景下的最优 JavaScript 压缩工具, 它努力在专用缩小工具的几个百分点内为大多数代码生成缩小的输出,当然也比其他工具快得多。

思考

当时用 esbuild 作为压缩器时需要记住以下几点:

Outdir

Supported by: Build

该配置项为 build 操作设置输出文件夹。例如,该命令会生成一个名为 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)
  }
}

输出文件夹如果不存在的话将会创建该文件夹,但是当其包含一些文件时不会被清除。 生成的文件遇到同名文件会进行静默覆盖。如果你想要输出文件夹只包含当前 esbuild 运行生成的文件, 你应该在运行 esbuild 之前清除输出文件夹。

如果你的构建有多个入口,且多个入口在单独的文件夹内,目录结构将从所有输入入口点路径中 最低的公共祖先 目录开始复制到输出目录中。If your build contains multiple entry points in separate directories, the 例如,这里有两个入口起点 src/home/index.tssrc/about/index.ts,输出文件夹将会包含 home/index.jsabout/index.js。 如果你想要自定义该行为,你应该改变 outbase directory

Outfile

Supported by: Build

该配置项为 build 操作设置输出文件名。这仅在单个入口点时适用。 如果有多个入口点,你必须适用 outdir 配置项来制定输出文件夹。 像这样使用outfile:

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

默认情况下,esbuild 的打包器为浏览器生成代码。 如果你打包好的代码想要在 node 环境中运行,你应该设置 platform 为 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)
  }
}

当 platform 设置为 browser(默认值)时:

当 platform 设置为 node 时:

当 platform 设置为 neutral 时:

可以查看 为浏览器打包为 node 打包

Serve

Supported by: Build

在开发过程中,当发生更改时在文本编辑器与浏览器之间来回切换是很正常的事。 在浏览器中重新加载代码之前手动重新运行 esbuild 是很不方便的。有几种方法可以自动完成:

此 API 调用使用的是最后一种方法。serve API 与 build API 调用很相似, 但是它不会将生成的代码写入到文件系统中,它启动一个 long-lived 本地 web 服务器来为最新构建生成的代码提供服务。 每批新的请求都会导致 esbuild 在响应请求之前重新运行构建命令,这样你的文件就总是最新的。

此方法对于其他方法的优势在于 web 服务器可以延迟浏览器请求,知道构建完成。 在最新构建完成之前重新加载你的代码,将永远不会运行上一次构建生成的代码。 这些文件在内存中提供服务,并且没有写入到文件系统中,以确保过时的文件不会被监听。

请注意,这仅会在开发环境中使用。不要将其用在生产环境中。 在生产环境中你不应该使用 esbuild 作为一个 web 服务器来服务静态资源。、

有两个不同的方法来使用 serve API:

方法 1:为 esbuild 构建出的所有内容提供服务

通过这种方法,你为 esbuild 提供一个名为 servedir 的目录,除了 esbuild 生成的文件之外,还提供了额外的内容。 这对于创建一些静态 HTML 页面并希望使用 esbuild 打包 JavaScript 和/或 CSS 的简单情况非常有用。 你可以把你的 HTML 文件置于 servedir 中,你的其他源代码置于 servedir 之外, 然后将 outdir 设置为在某处的 servedir:

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

在上面的例子中,你的 www/index.html 页面可以像这样引用打包好的 src/app.js 文件:

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

当你这样做时,每一个 HTTP 请求都会导致 esbuild 重新构建你的代码,并且为你提供最新版本的代码。 因此每次刷新页面,js/app.js 文件总是最新的。请注意尽管生成的出现在了 outdir 目录中, 但是它从来没有使用 serve API 真正写入过文件系统。相反,生成的代码映射的路径(即优先于其他路径) 在 servedir 和生成的文件直接从内存提供服务。

这样做的好处是,你可以使用在开发环境与生产环境使用完全相同的 HTML 页面。 在开发环境中你可以使用 --servedir= 运行 esbuild,esbuild 将会直接为生成的输出文件提供服务。 在生产环境中,你可以设置该标志,esbuild 将会把生成的文件写入到文件系统中。 这两种情况下你应该能够在浏览器中得到完全一样的结果,其在开发环境与生产环境中拥有完全相同的代码。

端口号默认自动设置为大于等于 8000 的一个开放端口。端口号会在 API 调用时返回(或者使用 CLI 时会打印在终端中), 这样你就可以知道应该访问哪一个 URL。如果有必要的话,端口号可以被指定(下面会详细描述)。

方法 2: 仅为 esbuild 生成的文件提供服务

使用该方法,你只需要告诉 esbuild 为 outdir 中的内容提供服务,而不会让其为额外的内容提供服务。 对比更复杂的开发环境配置是比较有用的。例如,你可能想要在开发环境中使用 NGINX 作为反向代理来路由不同的路径到不同的后端服务 (例如 /static/ 转向 NGINX、/api/ 转向 node、/js/ 转向 esbuild 等)。 可以向这样使用 esbuild 的该方法:

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

上面示例中的 API 调用会在 http://localhost:8000/out.js 为 编译好的 src/app.js 提供服务。就像是第一个方法,每个 HTTP 请求都会导致 esbuild 重新构建你的代码, 并且为你提供最新版本的代码。因此 out.js 将一直是最新的。你的 HTML 文件(被其他 web 服务器在其他端口上提供服务) 可以在你的 HTML 文件中像这样关联编译好的文件:

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

在没有启用 web 服务器的情况下,使用正常的构建命令时,web 服务器的 URL 结构与 输出目录 的URL结构完全相同。 例如,如果输出输出文件夹包含一个叫做 ./pages/about.js 的文件,web 服务器将会有一个相应的 /pages/about.js 路径。

如果你想要浏览 web 服务器以查看哪些 URL 是有效的,你可以通过访问文件夹名称而不是文件名称来使用内置文件夹列表。 例如,如果你正在 8000 端口运行 esbuild 的 web 服务器,你可以在浏览器中访问 http://localhost:8000/ 来查看 web 服务器的根目录。 在这里你可以点击链接在 web 服务器中打开不同的文件或链接。

参数

请注意 serve API 是与 build API 不同的 API 调用。 这是因为启动一个长时间运行的 web 服务器是完全不同的,因此需要不同的参数和返回值。 serve API 调用的第一个参数是一个带有特定于 serve 的配置项的配置项对象:

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

serve API 调用的第二个参数是每个请求都会调用的底层构建 API 的常规配置项集合。 查看 build API 文档获取更多关于这些配置项的信息。

返回值

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()
}

自定义服务器行为

不可能 hook 到 esbuild 的本地服务器来定制服务器本身的行为。 相反,应该通过在 esbuild 前设置代理来定制行为。

下面是一个代理服务器的简单示例。 他添加了自定义 404 页面而不使用 esbuild 的默认 404 页面:

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);
});

该代码在一个随机本地端口启动了 esbuild 服务器,然后在 3000 端口启动了一个代理服务器。 在开发环境中你可以在浏览器中加载 http://localhost:3000, 这将会走向代理。该示例示范了在 esbuild 已经处理请求之后修改相应信息,但你也可以在 esbuild 处理它之前 修改或替换请求信息。

你可以使用代理做很多事情,就像下面的举例:

如果你有更高级的需求的话,你也可以使用诸如 NGINX 一样真正的代理。

Sourcemap

Supported by: Transform | Build

Source map 可以使调试代码更容易。 它们编码从生成的输出文件中的行/列偏移量转换回 对应的原始输入文件中的行/列偏移量所需的信息。 如果生成的代码与原始代码有很大的不同, 这是很有用的(例如 你的源代码为 Typescript 或者你启用了 压缩)。 如果你更希望在你的浏览器开发者工具中寻找单独的文件, 而不是一个大的打包好的文件, 这也很有帮助。

注意 source map 的输出支持 JavaScript 和 CSS, 而且二者的配置一致。下文中提及的 .js 文件 和 .css 文件的配置是类似的。

启用 source map 将会伴随着任何一个生成的 .js 文件生成一个 .js.map 文件,并且在 .js 文件底部添加特殊的 //# sourceMappingURL= 注释以指向 .js.map 文件。 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:

There are four different modes for source map generation:
  1. linked

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

CLI JS Go
esbuild app.ts --sourcemap --outfile=out.js
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)
  }
}

如果输入的文件本身包含特殊 //# sourceMappingURL= 注释,esbuild 将会自动尝试解析 链接的 source map。如果成功的话,生成的源映射中的映射将一路映射回输入源映射中引用的原始源代码。

  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:

  1. external

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

CLI JS Go
esbuild app.ts --sourcemap=external --outfile=out.js
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)
  }
}

如果你想插入整个 source map 到 .js 文件中而不是单独生成一个 .js.map 文件,你应该设置 source map 模式为 inline

  1. inline

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

CLI JS Go
esbuild app.ts --sourcemap=inline --outfile=out.js
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)
  }
}

请记住 source map 通常是比较大的,因为他们包含所有的源代码,因此你通常不会想让代码中包含 inline source maps。为了移除 source map 中的源代码(只保存文件名以及行/列映射关系), 请使用 sources content 配置项。

如果你想同时设置 inlineexternal 的话,你应该设置 source map 模式为 both

  1. both

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

CLI JS Go
esbuild app.ts --sourcemap=both --outfile=out.js
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)
  }
}

使用 source maps

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.

在浏览器中,source maps 应该会自动被浏览器开发者工具选中,只要其启用了 source map 设置。 请注意浏览器仅在堆栈跟踪打印在控制台后才会使用 source maps。堆栈跟踪本身没有修改,所以检查 error.stack。 你代码中的堆栈仍然会给出包含已编译代码的未映射堆栈跟踪。 这里是如何启用浏览器开发者工具中 source map 设置的方式:

在 node 环境中,source map 在 version v12.12.0 版本之后原生支持。 此特性是默认关闭的,但是可以通过一个标志启用。不向在浏览器中,实际的堆栈跟踪也被修改了,所以检查你代码中的 error.stack 将会给出包含源代码的堆栈跟踪。这是如何在 node 中启用该设置(--enable-source-maps 标志 必须在文件名之前):

node --enable-source-maps app.js

代码分割

代码分隔仍然处于开发状态。它现在仅支持 esm 输出 格式。 使用 import 语句来分割代码块也有一个已知的 排序问题 。你可以关注 跟踪问题 获取此特性的更新。

启用 "代码分隔" 有两个目的:

当期启用代码分割时,你必须使用 outdir 配置输出文件夹。

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

此配置项设置生成 JavaScript 代码的目标环境。例如,你可以配置 esbuild 不要生成任何 v10 版本的 node 不能处理的新的 JavaScript 代码。target 可以设置为类似于 es2020 的 JavaScript 语言版本,或者一个引擎列表(目前可以是 chromefirefoxsafariedge 或者 node)。默认的 target 为 esnext,这意味着默认情况下,esbuild 将假设所有最新的 JavaScript 特性都是受支持的。

这里是在 esbuild 中使用所有可用 target 环境名称的例子。请注意你不需要指定所有这些; 你可以仅仅指定你的项目关心的目标环境的子集。如果你愿意的话,你也可以更精确地描述版本号 (例如 设置为 node12.19.0 而不仅仅是 node12):

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

你可以参考 JavaScript loader 获取更多关于哪个 语言版本引入了哪些语法特性。请记住尽管像 es2020 的 JavaScript 语言版本是以年定义的, 这是该规范被批准的年份。这与所有主流浏览器实现该规范的年份无关,因为实现该规范的时间往往 早于或晚于那一年。

请注意如果你使用了一个语法特性,esbuild 还不支持将其转为目标语言 target,esbuild 将会 在不支持的语法位置生成一个错误。例如,当目标是 es5 语言版本时,经常会出现这种情况, 因为 esbuild 只支持将大多数较新的 JavaScript 语法特性转换为 es6

Watch

Supported by: Build

在 build API 中启用监听模式,告诉 esbuild 监听文件系统中的变化,并在可能导致构建失效的 文件更改时重新构建。像这样使用它:

CLI JS Go
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...')
})
package main

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

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

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

  fmt.Printf("watching...\n")

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

如果你正在使用 JavaScript 或者 Go API,你可以选择性地提供一个回调函数,该函数将会在 增量构建完成后调用。一旦构建完成,就可以使用它来做一些事情(例如 重新加载浏览器中的应用):

JS Go
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...')
})
package main

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

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Outfile:     "out.js",
    Bundle:      true,
    Write:       true,
    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))
        }
      },
    },
  })

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

  fmt.Printf("watching...\n")

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

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:

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

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

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

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

  fmt.Printf("watching...\n")

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

为了实现可移植性,esbuild 中的监视模式使用轮询而不是特定于操作系统的文件系统 api 来实现的。 与一次扫描整个目录树的更传统的轮询系统相比,轮询系统被设计为使用相对较少的 CPU。 仍然会定期扫描文件系统,但每次扫描只检查文件的随机子集,这意味着在更改之后,文件的更改将很 快被发现,但不一定是立即发现。

使用当前的启发式方法,大型项目应该每 2 秒完全扫描一次,因此在最坏的情况下,可能需要 2 秒才能注意到变化。 然而,在注意到变更后,变更的路径会出现在最近变更的路径的短列表中,每次扫描都会检查这些路径,所以对最近 变更的文件的进一步变更应该几乎立即被注意到。

请注意,如果你不想使用基于轮询的方法,你可以使用 esbuild 的 增量构建 API 跟一个 你选择的文件监听器来实现监听模式。

Write

Supported by: Build

build API 可以写入文件系统中,也可以返回本应作为内存缓冲区写入的文件。默认情况下 CLI 与 JavaScript API 写入到文件系统,GO API 不是。使用内存缓冲区:

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

高级配置

Allow overwrite

Supported by: Build

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

CLI JS Go
esbuild app.js --outdir=. --allow-overwrite
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:

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

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

资源名称

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

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

  out.js                                                                    27.4kb  100.0%
   ├ node_modules/react-dom/cjs/react-dom-server.browser.production.min.js  19.2kb   70.2%
   ├ 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:

CLI JS Go
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.2%
   │  └ 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.

资源名称

loader 设置为 file 时,该配置项 控制额外生成的文件名称。它使用带有占位符的模板来配置输出路径,当生成输出路径时,占位符将被特定 于文件的值替换。例如,例如,指定 assets/[name]-[hash] 的资源名 称模板,将所有资源放入输出目录内名为 assets 的子目录中,并在文件名中包含资产的内容哈希。 像这样使用它:

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

在资源路径模板中有4个可用占位符:

资源路径模板不需要包含文件拓展名。资源的原始拓展名将会在模板替换完成后添加到输出路径尾部。

该配置项与 chunk 名称入口名称 相似。

Supported by: Transform | Build

使用它可以在生成的 JavaScript 和 CSS 文件的开头插入任意字符串。这一般被用来插入注释:

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

这与 footer 很相似,只不过它是在末尾插入而不是开头。

请注意如果你在 CSS 文件中插入了一段非注释代码,CSS 会忽略 non-@import 规则后面的 @import 规则(@charset 规则除外),所以使用 Banner 来注入 CSS 规则可能会意外地禁用外部样式表的导入。

Charset

Supported by: Transform | Build

默认情况下 esbuild 的输出是 ASCII-only。任何非 ASCII 字符都使用反斜杠转义序列进行转义。 原因是默认情况下,非 ASCII 字符会被浏览器误读,导致混淆。你必须在你的 HTML 文件中明确添加 <meta charset="utf-8">,或者为他提供正确的 Content-Type 头,以便浏览器不会损坏代码。另一个原因是,非 ASCII 字符会显著 降低浏览器解析器的速度。 然而,使用转义序列会使生成的输出稍微大一些,也会使其更难阅读。

如果你想让 esbuild 在不使用转义序列的情况下打印原始字符,并且你已经确保浏览器将你的代码解释为 UTF-8, 你可以通过设置字符集来禁用字符转义:

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

一些警告:

Chunk 名称

此选项控制在启用 代码分割 时自动生成的共享代码块的文件名。 它使用带有占位符的模板来配置输出路径,当生成输出路径时,占位符将被特定于 chunk 的值替换。 例如,指定 chunks/[name]-[hash] 的 chunk 名称模板, 将所有生成的块放入输出目录内的名为 chunks 的子目录中,并在文件名中包含 chunk 的内容哈希。 像这样使用它:

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

在 chunk 路径模板中有3个可用占位符:

chunk 路径模板不需要包括一个文件拓展名。在模板替换之后,为适当内容类型配置的 out extension 将自动添加到输出路径的末尾。

注意,这个配置项只控制自动生成的共享代码块的名称。它 控制与入口点相关的输出文件的名称。 它们的名称目前是从相对于 outbase 目录的原始入口点文件的路径确定的,且无法更改此行为。 将来会添加一个额外的API选项,允许你更改入口点输出文件的文件名。

该配置项与 资源名称入口名称 相似。

颜色

该配置项启用或禁用 esbuild 写入终端中的 stderr 文件描述符中的错误和警告消息中的颜色。 默认情况下,如果 stderr 是一个 TTY 会话,颜色将自动启用,否则将自动禁用。 esbuild 中有颜色的输出就像是这样:

[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

将 color 设置为 true 可以强制启用有颜色的输出。 如果你自己把 esbuild 的 stderr 输出管道到 TTY 中,这是很有用的:

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

可以将 color 设置为 false 以禁用。

Conditions

Supported by: Build

此特性控制 package.json 中的 exports 字段是如何被解析的。通过 conditions 设置可以添加自定义条件。 你可以指定任意数量的包,这些包的含义完全取决于包的作者。Node 目前只认可了推荐使用的 developmentproduction 定制条件。下面是一个添加自定义条件 custom1custom2 的示例:

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

conditions 是如何工作的

Conditions 允许你在不同的情况下将相同的导入路径重定向到不同的文件位置。 包含条件和路径的重定向映射存储在包的 package.json 的 exports 字段中。 例如,在使用 importrequire 条件下会重新映射 require('pkg/foo')pkg/required.cjsimport 'pkg/foo'pkg/imported.mjs

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

Conditions 按照它们在 JSON 文件中出现的顺序进行检查。 所以上面的例子有点像这样:

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

默认情况下,esbuild 有五种内置特定行为,并且不能被禁用:

请注意当你使用 requireimport 条件时,你的包可能会在 bundle 过程中多次终止! 这是一个微妙的问题,它可能会由于代码状态的重复副本而导致 bug,此外还会使结果包膨胀。 这通常被称为 dual package hazard。 避免这种情况的主要方法是将所有代码都放在 require 条件中,而 import 条件只是一个轻包装器, 它调用包上的 require ,并使用 ESM 语法重新导出包。

Drop

Supported by: Transform | Build

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

CLI JS Go
esbuild app.js --drop:debugger
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  drop: ['debugger'],
})
package main

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

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

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}
CLI JS Go
esbuild app.js --drop:console
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  drop: ['console'],
})
package main

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

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

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

入口名称

该配置项控制与每一个入口文件相对应的输出文件的名称。它使用带有占位符的模板来配置输出路径, 占位符会在输出路径生成后被特定的值替换。例如,指定一个 [dir]/[name]-[hash] 的入口名称模板, 其在文件名中包含输出文件的哈希值,并且将文件置于输出目录中,也可能在子目录下 (查看下面关于 [dir] 的更多信息),像这样使用它:

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

在入口路径模板中有4个可用占位符:

入口路径模板不需要包含一个文件拓展名。根据文件类型,适当的 out 扩展名 将在模板替换 后自动添加到输出路径的末尾。

该配置项与 asset nameschunk names 相似。

Supported by: Transform | Build

使用它可以在生成的 JavaScript 和 CSS 文件的末尾插入任意字符串。这通常用于插入注释:

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

这与 banner 很相似,只不过它是在开头插入而不是末尾。

全局名称

该配置项仅在 format 设置为 iife(代表立即执行函数表达式)时有效。 它设置了全局变量的名称,用于存储从入口点导出的文件:

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

iife 格式指定全局名称将会生成下面的代码:

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

全局名称也可以是一个复合属性表达式,在这种情况下,esbuild 将生成一个具有该属性的全局变量。 冲突的现有全局变量将不会被覆盖。这可以用来实现“命名空间”,其中多个独立脚本将它们的导出添加到同一个全局对象中。 示例:

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

上面使用的复合全局名称生成的代码如下:

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:

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

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

CLI JS Go
esbuild app.js --bundle --ignore-annotations
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.

增量

如果用例使用相同的选项重复调用 esbuild 的 build API,你可能想要使用这个API。 例如,如果你正在实现文件监听服务,这是很有用的。 增量构建比常规构建更有效,因为一些数据被缓存,如果原始文件自上次构建以来没有更改,则可以重用这些数据。 增量构建 API 目前使用两种形式的缓存:

下面是如何进行增量构建:

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

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:

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

这将设置为每个 JSX 元素调用的函数。通常 JSX 表达式如下:

<div>Example text</div>

编译为一个 React.createElement 函数的调用,就像这样:

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

除了 React.createElement 函数之外,你还可以通过改变 JSX 工厂函数来调用其他东西。 例如,调用函数 h(在其他库中使用的函数,例如 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)
  }
}

另外,如果你正在使用 TypeScript,你可以把 JSX 添加到你的 tsconfig.json 文件中, 从而为 TypeScript 配置 JSX,esbuild 应该会自动获取它,而不需要进行配置:

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

JSX fragment

Supported by: Transform | Build

这将设置为每个 JSX 片段调用的函数。通常 JSX 片段表达式如下:

<>Stuff</>

编译成像这样的 React.Fragment 组件的用法:

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

除了 React.Fragment 之外,你还可以通过改变 JSX fragment 来使用其他组件。 例如,使用 Fragment 组件(在像 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)
  }
}

另外,如果你正在使用 Typescript,你可以通过添加该配置到你的 tsconfig.json 文件中 来为 Typescript 配置 JSX,esbuild 应该会自动获取它,而不需要进行配置:

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

Keep names

Supported by: Transform | Build

在 JavaScript 中,函数与类的 name 属性默认为源码中的附近标识符。这些语法形式都将函数 的 name 属性设置为 "fn"

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

然而,压缩 为了减小代码体积会进行重命名,并且 打包 有时候也要通过 重命名来避免冲突。在很多情况下都会改变 name 属性的值。这通常可以接受,因为 name 属性 正常情况下仅用于 debug。然而,一些框架为了注册和绑定依赖于 name 属性。如果是这样的话, 你可以启动该配置以保存原有的 name 值,甚至是在压缩的代码中:

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

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:

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

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

日志级别

可以修改日志级别,以阻止 esbuild 在终端中打印警告/错误信息。 六个日志级别分别是:

你可以通过如下方式设置日志级别:

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

日志限制

默认情况下,esbuild 会在已经报告 10 条信息的情况下停止报告日志信息。这避免了意外生成大量 的日志消息,这些消息可以很容易地锁定较慢的终端模拟器,如 Windows 命令提示符。它也避免了 意外地使用终端模拟器的整个滚动缓冲区有限的滚动缓冲区。

日志限制可以改为另外一个值,并且可以通过设置为 0 将其完全禁用。这会显示所有日志消息:

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

当你在 node 中导入一个包时,包中的 package.json 文件的 main 字段会决定导入哪个文件 (还有 很多其他的规则)。 包括 esbuild 在内的主流打包器允许你在解析包是额外指定一个 package.json 字段。 通常至少有三个这样的字段:

默认的 main 字段依赖于当前 platform 设置,本质上是 browser,module,main(浏览器)与 main,module(node)。 这些默认值应该与现有的包生态系统最广泛地兼容。但是如果你想的话,你可以像这样定制它们:

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

对于包作者:如果你想发布一个使用 browser 字段结合 module 字段填充 所有 CommonJS-vs-ESM 和 browser-vs-node 兼容性矩阵的包,你会想要使用 browser 字 段的扩展形式字段映射,而不只是一个字符串:

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

Mangle props

Supported by: Transform | Build

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

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

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

CLI JS Go
esbuild app.js --mangle-props=_$
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  mangleProps: /_$/,
})
package main

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

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

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

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

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

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

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

Quoted properties

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

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

CLI JS Go
esbuild app.js --mangle-props=_$ --mangle-quoted
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  mangleProps: /_$/,
  mangleQuoted: true,
})
package main

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

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

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

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

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

Preventing renaming

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

CLI JS Go
esbuild app.js --mangle-props=_$ "--reserve-props=^__.*__$"
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  mangleProps: /_$/,
  reserveProps: /^__.*__$/,
})
package main

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

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

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

Persisting renaming decisions

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

For example, consider the following input file:

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

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

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

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

CLI JS Go
esbuild app.js --mangle-props=_$ --mangle-cache=cache.json
let result = require('esbuild').buildSync({
  entryPoints: ['app.js'],
  mangleProps: /_$/,
  mangleCache: {
    customRenaming_: "cR_",
    disabledRenaming_: false
  },
})

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

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

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

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

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

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

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

And the following updated mangle cache:

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

Metafile

Supported by: Build

该配置告诉 esbuild 以 JSON 格式生成一些构建相关的元数据。 下面的例子就是将元数据置于名为 meta.json 的文件中:

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

该数据可以被其他工具分析。例如,bundle buddy 可以消费 esbuild 生成的元数据格式,并且生成包中模块的 treemap 可视化,以及每个模块所占用的空间。

元数据的 JSON 格式看起来像是这样(使用 TypeScript 接口进行描述):

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 的模块解析算法支持一个名为 NODE_PATH 的环境变量,该变量包含在解析导入路径时使用的全局目录列表。除了所有父目录中的 node_modules 目录之外, 还会在这些路径中搜索包。你可以在 CLI 中使用环境变量,在 JS 和 Go api 中使用数组将这个目录列表传递给 esbuild:

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

如果你正在使用 CLI 并且想要使用 NODE_PATH 传递多个文件夹的话,你必须在 : 或者在 Windows 中使用 ; 分隔它们。 这也是 Node 使用的格式。

Out extension

Supported by: Build

该配置项可以让你自定义文件的文件拓展名,这样 esbuild 可以生成布置 .js 或者 .css 文件。 除此之外,.mjs.cjs 拓展名在 Node 中有特殊含义(他们分别表示一个文件是 ESM 还是 CommonJS 格式)。 如果你正在使用 esbuild 生成多个文件的话该配置项是非常有用的,并且比必须使用 outdir 配置项而不是 outfile。你可以像这样使用它:

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

如果你的构建包含多个入口点,这些入口点都在单独的文件夹中,目录结构将被复制到相对于 output directory 目录的输出目录中。 例如,如果有 src/pages/home/index.tssrc/pages/about/index.ts 两个入口点,并且 outbase 目录为 src, 输出目录将会包含 pages/home/index.jspages/about/index.js。 下面是如何使用它:

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

如果没有指定 outbase 文件夹,它默认为所有入口起点路径的 最低共有祖先 目录。 上面的例子是 src/pages,这意味着输出目录将会包含 home/index.jsabout/index.js

Supported by: Build

这设置与 node 中的 --preserve-symlinks 设置相映射。 如果你使用这个设置(或者是 webpack 中的相似配置 resolve.symlinks), 你也会需要在 esbuild 中启用该设置。可以像这样启用:

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

启用此设置将导致 esbuild 根据原始文件路径(即没有符号链接的路径)而不是真实文件路径 (即有符号链接的路径)确定文件标识。这对于某些目录结构是有益的。请记住,这意味着如果 有多个符号链接指向一个文件,那么它可能被赋予多个身份,这可能导致它在生成的输出文件中出现多次。

注意:术语 "symlink" 的意思是 symbolic link, 它指的是一种文件系统特性,其中路径可以重定向到另一个路径。

Public path

Supported by: Build

这与 external file loader 结合会很有用。 默认情况下,loader 使用 default 导出将导入文件的名称导出为字符串。public path 配置项 允许你在这个 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

各种各样的 JavaScript 工具都有一个约定,如果在一个新的表达式调用之前有一个包含 /* @__PURE__ */ 或者 /* #__PURE__ */ 的特殊注释,那么就意味着那个表达式 在其返回值在没有使用的情况下可以被移除。就像是这样:

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

像 esbuild 这类打包器在 tree shaking(又名 无用代码移除)期间使用该信息, 在由于 JavaScript 代码的动态特性,打包器不能自己证明删除未使用的导入是安全的情况下, 跨模块边界执行细粒度的删除。

请注意,虽然注释说的是 "pure",但令人困惑的是,它并没有表明被调用的函数是纯的。 例如,它并不表示可以缓存对该函数的重复调用。 这个名字本质上只是“如果不用就可以删除”的抽象简写。

一些表达式,比如JSX和某些内置全局变量,在 esbuild 中会自动注释为 /* @__PURE__ */。 你也可以配置其他的全局变量标记为 /* @__PURE__ */。例如,你可以将全局的 console.log 函数标记为这样, 只要结果没有被使用,当 bundle 被缩小时,它就会自动从你的 bundle 中删除。

值得一提的是,注释的效果只扩展到调用本身,而不扩展到参数。关于副作用的参数仍然保存:

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

node 使用的解析算法 支持隐式的文件扩展名。你可以 require('./file'),然后他将会按照顺序检查 ./file./file.js./file.json./file.node。包括 esbuild 在内的现代打包器 将此概念拓展到了其他文件类型。在 esbuild 中可以使用解析插件设置对隐式文件拓展名进行自定义配置, 默认为 .tsx,.ts,.jsx,.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)
  }
}

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

Source root

Supported by: Transform | Build

该特性仅在启用 source maps 时才相关。它允许你在 source map 中设置 sourceRoot 字段的值, 该值指定 source map 中所有其他路径的相对路径。如果该字段不存在,则 source map 中的所有路径将被解释为相对于 包含 source map 的目录。

你可以像这样配置 sourceRoot

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

该配置可以让你在使用一个没有文件名的输入时设置文件名。这将会在 stdin 中使用 transform API 以及 build API 时会出现这种情况。配置的文件名反映在错误消息和 source maps 中。如果没有配置,该文件名默认为 <stdin>。 你可以像这样配置:

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 map 格式的 第三版 生成 source maps, 这是目前最广泛支持的变体。每一个 source map 看起来像这样:

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

sourcesContent 为可选字段,其包含所有的源代码。这对 debug 非常有用,因为它意味着 源代码在调试器上处于可用状态。

但是,在某些场景中并不需要它。例如,如果你只是在生产环境中使用源代码映射来生成包含原始 文件名的堆栈跟踪,那么你不需要原始源代码,因为没有涉及到调试器。 在这种情况下,可以省略 sourcesContent 字段,使 source map 更小:

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

通常,build API 调用接受一个或多个文件名作为输入。但是,这个配置项可以用于在文件系统上根本不存在模块 的情况下运行构建。它被称为 "stdin",因为它对应于在命令行上用管道将文件连接到 stdin。

除了指定 stdin 文件的内容之外,你还可以选择性地指定解析目录(用于确定相对导入的位置)、 sourcefile(在错误消息和源映射中使用的文件名)和 loader (用于确定如何解释文件内容)。CLI 没有指定解析目录的方法。相反,它被自动设置为当前工作目录。

这里是如何使用该特性的方法:

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 是 JavaScript 社区用来描述无用代码消除的术语, 这是一种常见的编译器优化,可以自动删除无法访问的代码。注意,esbuild 中的 tree shaking 在绑定期间总是启用的,而且不能关闭,因为在不改变可观察行为的情况下,移除未使用的代码会使结果文件变小。

用一个例子来解释 tree shaking 是最简单的。考虑以下文件。有一个已使用的函数和一个未使用的函数:

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

如果你是用 esbuild --bundle input.js --outfile=output.js 打包该文件, 没有使用到的函数将会自动销毁,并为你产生以下输出:

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

即使我们将函数分割成一个单独的库文件并使用 import 语句导入它们也是有效的:

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

如果你是用 esbuild --bundle input.js --outfile=output.js 打包该文件, 没有使用到的函数将会自动销毁,并为你产生以下输出:

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

// input.js
one();

通过这种方式,esbuild 将只打包你实际使用的部分库,这有时可以节省大量的大小。 注意,esbuild的 tree shaking 实现依赖于使用 ECMAScript 模块 importexport 语句。 它不能与 CommonJS 模块一起工作。npm 上的许多库都包含了这两种格式,esbuild 会在默认情况下选择适合 tree shaking 的格式。你可以使用 main fields 配置项自定义 esbuild 选择的格式。

由于 JavaScript 是一门动态语言,对于编译器来说确定未使用的代码是一件很困难的事情,所以 社区发展出了某些注释来帮助编译器确定哪些代码是未使用的。目前 esbuild 支持两种 tree-shaking 注释:

这些注释可能会产生问题,因为编译器完全依赖于开发人员来确保准确性,而开发人员偶尔会发布带有 不正确注释的包。sideEffects 字段对于开发人员来说特别容易出错,因为默认情况下, 如果没有使用导入,它会导致包中的所有文件都被认为是无用代码。如果你添加了一个包含副作用的新文件, 并且忘记更新该字段,那么当人们试图打包它时,你的包可能会崩溃。

所以 esbuild 包含一种忽略 tree-shaking 注释的方法。只有当你遇到一个问题, bundle 因为意外地从 bundle 中删除了必要的代码而破坏时,你才应该启用这个功能: 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: By default, tree shaking is only enabled either when bundling is enabled or when the output format is set to iife, otherwise tree shaking is disabled. You can force-enable tree shaking by setting it to true:

CLI JS Go
esbuild app.js --tree-shaking=true
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:

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

启用该配置意味着 esbuild 不再支持 /* @__PURE__ */ 注释与 sideEffects 字段。 然而它仍会对未用到的导入做自动 tree shaking,因为这不会依赖开发者注释。理想情况下, 这个标志只是一个临时的解决方案。你应该向包的维护者报告这些问题以修复它们, 因为它们表明了包的一个问题,而且它们可能也会使其他人出错。 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. 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

正常情况下 build API 会自动发现 tsconfig.json 文件,并且在构建时读取其内容。 然而,你也可以配置使用一个自定义 tsconfig.json 文件。如果你需要对同一份代码针对不同的设置 做多次打包时会非常有用:

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

该配置项可以被用来将你的 tsconfig.json 文件传递给 transform API, 其不会访问文件系统。像这样使用它:

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

Working directory

Supported by: Build

这个 API 配置允许你指定用于构建的工作目录。它通常默认为用于调用 esbuild 的 API 的进程的 当前工作目录。 esbuild 使用工作目录做一些不同的事情,包括将作为 API 配置给出的相对路径解析为绝对路径, 以及将绝对路径解析为日志消息中的相对路径。下面是如何覆盖它:

JS Go
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 特殊细节

由于 JavaScript 是单线程的,因此有几种不同的方法来调用 API,这些方法具有不同的性能和方便性。 重要的是要意识到这些差异,以正确选择的一个适合你的情况。

首先是同步 API。这是最方便的选项,因为单线程 JavaScript 代码具有最干净的语法。 如果你所需要做的只是运行 esbuild 然后退出,那么它也是最佳性能的。 但是,它阻塞了主线程,所以如果你在此期间有其他工作要执行,你就不希望使用它。 这也是唯一不能使用插件的选项(因为插件是异步的)。它是这样的:

Sync API

Synchronous API calls return their results inline:

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

然后是异步 API。每个调用返回一个 promise,而不是立即完成。在底层,esbuild 二进制文件作为一个子进程生成, 在宿主进程中的所有调用之间共享。主机使用自定义二进制协议通过 stdin、stdout 和 stderr 管道与子进程通信。 如果你只需要运行esbuild一次,但又需要在后台做其他工作,那么这是理想的。 它还允许你并发地运行许多 esbuild API 调用,然后将这些调用分散到所有可用的核心上,以获得最佳性能。 使用它看起来像这样: Pros:

Cons:

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:

Cons:

esbuild API 也可以在 Web Worker 中使用 WebAssembly 运行。为了使用它你需要安装 esbuild-wasm 而不是 esbuild

npm install esbuild-wasm

esbuild 在浏览器中的 API 与 node 中的类似,你需要首先调用 initialize(),然后你需要传递 WebAssembly 二进制文件的 URL。API 的同步版本也是不可用的。假如你正在使用一个打包器,那么它看起来应该是这样:

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 => { ... })
})

如果你已经在 worker 中运行改代码而不像运行 initialize 创建另一个 worker,你可以向其传递 worker: false。然后,它会在调用 initialize 的线程中创建一个 WebAssembly 模块。

你还可以在 HTML 文件中将 esbuild 的 API 作为 script 标签使用,而不需要通过注入 lib/browser.min.js 文件来使用打包器。在这种情况下,API 创建了一个全局变量 esbuild,它保存了API对象:

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

如果你需要通过 ECMAScript 模块使用 API,你应该导入 esm/browser.min.js 文件:

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