API

可以通过如下三种方式调用 API:在命令行中调用,在 JavaScript 中调用, 在 Go 中调用。 概念和参数在这三种方式中基本相同。

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

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

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 示例:

echo 'let x: number = 1' | esbuild --loader=ts
let 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)。

Build API

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

echo 'let x: number = 1' > in.ts
esbuild in.ts --outfile=out.js
cat out.js
let 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)读取单个输入文件。

一般配置项

Bundle

Supported by: Build

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

esbuild in.js --bundle
require('esbuild').buildSync({
  entryPoints: ['in.js'],
  bundle: true,
  outfile: 'out.js',
})
package main

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

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

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

请注意打包与文件连接不同。在启用打包时向 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

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

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

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

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

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

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

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

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

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

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

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

如果你在使用命令行工具,请记住,不同的 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。你可以使用常规的 JavaScript 语法来消除跨平台差异。

Entry points

Supported by: Build

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

另外,你也可以用下面的写法来为每一个入口文件定义一个出口文件名称

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

这将生成两个出口文件,out/out1.js 和 out/out2.js。

External

Supported by: Build

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

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

echo 'require("fsevents")' > app.js
esbuild 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'],
})
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)路径中使用 * 通配符时, 该模式将应用于源代码中的原始路径,而不是解析为实际文件系统路径后的路径。 这允许你匹配不是真实文件系统路径的路径。

Format

Supported by: Transform | Build

为生成的 JavaScript 文件设置输出格式。有三个可能的值:iife、cjs 与 esm。

IIFE

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

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 环境中运行。它假设环境包括 exports、 require 与 module。在 ECMAScript 模块语法中带有导出的入口点将被转换为一个模块, 每个导出名称的 “exports” 上都有一个 getter。当你设置 platform 为 node 时, cjs 为默认格式。像这样使用它:

echo 'export default "test"' | esbuild --format=cjs
# ...
# __export(exports, {
#   default: () => stdin_default
# });
# var stdin_default = "test";
let js = 'export default "test"'
let out = require('esbuild').transformSync(js, {
  format: 'cjs',
})
process.stdout.write(out.code)
package main

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

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

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

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

ESM

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

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,那么这是自动进行的。否则:

  • 在浏览器中,你可以使用 <script src="file.js" type="module"></script> 加载模块。

  • 在 node 环境中,你可以使用 node --experimental-modules file.mjs 加载模块。 请注意 node 需要 .mjs 拓展名,除非你在 package.json 文件中配置了 "type": "module" 。 你可以使用 esbuild 中的 out extension 设置来自定义生成文件的拓展名。 你可以点击 这里 获取更多关于使用 ECMAScript modules 的内容。

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。

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

你可以用 define 属性来做 process.cwd 到 dummy_process_cmd 的映射

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 公约open in new window, esbuild 中该公约对所有的导入文件生效,而不仅仅是注入文件。

Loader

Supported by: Transform | Build

该配置项改变了输入文件解析的方式。例如, js loader 将文件解析为 JavaScript, css loader 将文件解析为 CSS。

配置一个给定文件类型的 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 调用进行打包,就像这样:

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, 就像这样:

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),就像这样:

echo 'let x: number = 1' | esbuild --loader=ts
# let 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 中这样启用压缩:

echo 'fn = obj => { return obj.x }' | esbuild --minify
# fn=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)
  }
}

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

echo 'fn = obj => { return obj.x }' | esbuild --minify-whitespace
# fn=obj=>{return obj.x};
echo 'fn = obj => { return obj.x }' | esbuild --minify-identifiers
# fn = (n) => {
#   return n.x;
# };
echo 'fn = obj => { return obj.x }' | esbuild --minify-syntax
# fn = (obj) => obj.x;
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:

echo 'div { color: yellow }' | esbuild --loader=css --minify
# div{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 作为压缩器时需要记住以下几点:

  • 当启用压缩时也可能也需要设置 target 配置项 。默认情况下, esbuild 利用现代 JavaScript 特性使你的代码变得更小。例如, a === undefined || a === null ? 1 : a 可以被压缩为 a ?? 1。如果你不想让 esbuild 在压缩是利用现代 JavaScript 特性, 你应该使用一个更老的语言目标,例如 --target=es6。

  • 对于所有的 JavaScript 代码, 压缩不是 100% 安全的。 这对 esbuild 和其他流行的 JavaScript 压缩器, 例如 terseropen in new window 是真实存在的。 特别地,esbuild 并不是为了保存函数上调用 .toString() 的值而设计的。 原因是如果所有函数中的所有代码都要逐字保存的话, 压缩会变得非常困难并且实际上是没有用的。 然而,这意味着依赖于 .toString() 返回值的 JavaScript 代码 在压缩过程中可能会中断。 例如,在 Angularopen in new window 框架中一些模式在代码压缩时会中断, 因为 Angular 使用 .toString() 来读取函数的参数名称。 一个解决方案是使用 明确的注释替代open in new window

  • 默认情况下,esbuild 不会在函数和类对象上保留 .name 的值。这是因为大多数代码不会依赖该属性, 并且使用更短的名称是一个重要的大小优化。然而,一些代码确实依赖 .name 属性来注册和绑定。 如果你需要依赖该属性,你应该启用 keep names 配置项。

  • 使用某些 JavaScript 特性可以禁用 esbuild 的许多优化,包括压缩。 具体来说,直接使用 eval 和/或 with 语句可以阻止 esbuild 将标识符重命名为更小的名称, 因为这些特性会导致标识符绑定在运行时而不是编译时发生。 这几乎总是无意的,而且只发生在人们不知道什么是直接 eval 以及它为什么不好的情况下。

    如果你正在考虑像这样写一段代码:

    // 直接使用 eval (将会禁用整个文件的压缩)
    let result = eval(something)
    

    你应该这样写,你的代码才能被压缩:

    // 间接使用 eval (对周围的代码没有副作用)
    let result = (0, eval)(something)
    
  • esbuild 中的压缩算法还没有进行高级代码优化。特别是,下面的代码优化对 JavaScript 是 有可能的,但不是由 esbuild 完成的(不是一个详尽的列表):

    • 消除函数体中的 dead-code
    • 函数内联
    • Cross-statement 常数传播
    • 对象形状建模
    • Allocation sinking
    • Method devirtualization
    • Symbolic execution
    • JSX 表达式提升
    • TypeScript 枚举检测和内联

如果你的代码使用的模式要求某些形式的代码优化是紧凑的,或者如果你正在为你的用例搜索最佳的 JavaScript 压缩算法,你应该考虑使用其他工具。实现这些高级代码优化的一些工具示例包括 Terseropen in new windowGoogle Closure Compileropen in new window

Outdir

Supported by: Build

该配置项为 build 操作设置输出文件夹。例如,该命令会生成一个名为 out 的目录:

esbuild app.js --bundle --outdir=out
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  outdir: 'out',
})
package main

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

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

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

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

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

Outfile

Supported by: Build

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

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:

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(默认值)时:

  • 默认的输出 格式 为 iife,将生成的 JavaScript 代码包裹在立即执行函数表达式中, 以阻止变量泄露到全局作用域中。
  • 如果一个包在 package.json 文件中 的browser 配置配置了一个 map,esbuild 将会使用该 map 替换指定的文件或模块为对浏览器友好的版本。 例如,一个包可能会用 path-browserifyopen in new window 替换 pathopen in new window
  • main fields 设置为 browser,module,main, 但是会有一些额外的特殊行为。如果你个包支持 module 与 main,但是不支持 browser, 那么当使用 require() 导入时,将使用 main 而不是 module。 此行为通过将函数赋值给 module.exports 来改善与导出函数的 CommonJS 模块的兼容性。
  • conditions 设置自动包含了 browser 情况。 这将改变 package.json 文件中 exports 字段如何被解释为偏好特定于浏览器代码的方式。

当 platform 设置为 node 时:

  • 默认输出 格式 为 cjs,代表 CommonJS(node 使用的模块格式)。 ES6-风格的导出使用的 export 语句将会被转换为 CommonJS exports 对象中的 getters。

  • 所有诸如 fs 的 内置 node 模块open in new window 会被自动标记为 external,因此在打包器尝试打包他们时不会导致错误。

  • main 字段 设置为 main,module。 这意味着 tree shaking 操作可能不会发生在同时提供 module 和 main 的包中, 因为 tree shaking 操作只适用于 ECMAScript 模块,而不适用于 CommonJS 模块。

    不幸的是,一些包将 module 视为 "browser code" 而不是 "ECMAScript module code", 因此,这种默认行为是兼容性所必需的。如果你想要启用 tree shaking 并且知道这样做是安全的, 那么你可以手动将 main 字段 设置为 module,main。

  • conditions 设置自动包含 node 情况。 这将改变 package.json 文件中 exports 字段如何被解释为偏好特定于 node 端代码的方式。

当 platform 设置为 neutral 时:

  • 默认输出 格式 为 esm,使用 ECMAScript 2015 (即 ES6) 中引入的 export 语法。 如果默认值不合适的话你可以改变输出格式。
  • main 字段 默认设置为空。如果你想使用 npm 风格的包, 你可能需要将其配置为其他内容,比如将 node 使用的 main 字段配置为 main。
  • conditions 设置不会自动包含任何平台特定值。

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

Serve

Supported by: Build

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

  • 当一个文件发生更改时使用 监听模式 重新运行 esbuild
  • 将你的文本编辑器配置为每次保存代码重新运行 esbuild
  • 使用一个对于每次请求都会重新构建的 web 服务器来为你的代码提供服务

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

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 的该方法:

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 的配置项的配置项对象:

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

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

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

    可以选择在这里配置 HTTP 端口。如果省略,它将默认使用一个开放端口,优先级为 8000 端口。 你可以在命令行中通过使用 --serve=8000 而不仅仅是 --serve 来设置端口。

  • host

    默认情况下,esbuild 在所有的 IPv4 网络接口中有效。 这对应着 0.0.0.0 的 host。如果你想要配置一个不同的 host(例如,仅在 127.0.0.1 回路中提供服务而不向网络公开任何信息), 你可以使用该参数指定 host。你可以在命令行中通过使用 --serve=127.0.0.1:8000 而不仅仅是 --serve 来设置 host。

    如果你需要使用 IPv6 而不是 IPv4,你仅需要指定一个 IPv6 host 地址。 在 IPv6 中与 127.0.0.1 回路接口等效的是 ::1,并且与 0.0.0.0 通用接口等效的是 ::。 如果你正在使用命令行将 host 设置为 IPv6 地址,你需要用方括号将 IPv6 地址括起来,以区别地址中 的冒号和分隔主机和端口的冒号,就像这样: --serve=[::]:8000。

  • servedir

    这是 esbuild 的 HTTP 服务器提供的额外内容目录,当传入的请求与生成的任何输出文件路径不匹配时,它将代替 404。 这使你可以将 esbuild 用作通用的本地 web 服务器。例如,使用 esbuild --servedir=. 在 localhost 为当前文件夹提供服务。在前面关于不同方法的部分中,对使用 servedir 进行了更详细的描述。

  • onRequest

    对每个传入的请求调用一次,并提供有关请求的一些信息。 CLI 使用该回调为每一个请求打印日志信息。time 字段是为该请求生成数据的时间, 但是它不包括向客户端发送请求流的时间。

    请注意这会在请求完成后调用。使用该回调以任何形式修改请求是不可能的。 如果你想要这样做,你应该 在 esbuild 之前设置一个代理

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

返回值

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

    这个是最终被 web 服务器使用的端口。如果你没有指定一个端口的话你将想要使用它,因为 esbuild 将最终挑选一个随机开放端口,并且你需要知道它选择了那个段扩才能连接到它。 如果你正在使用 CLI,这个端口号将会被打印在终端的 stderr 中。

  • host

    这个是最终被 web 服务器使用的 host。它将是 0.0.0.0(即 服务所有可用的网络接口),除非配置了一个自定义 host。

  • wait

    只要 socket 打开,serve API 调用会立即返回。这个 wait 返回值提供了一种方法,可以在 web 服务器 由于网络错误或者由于将来某个时间点停止调用而终止是通知它。

  • stop

    当你不需要它清理资源时,你可以调用该回调以停止 web 服务器。 这将会立即终止所有打开的链接,并且唤醒所有等待 wait 返回值的代码。

自定义服务器行为

不可能 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 处理它之前 修改或替换请求信息。

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

  • 插入你自己的 404 页面(上面的示例)
  • 自定义路由与文件系统中文件之间的映射关系
  • 重定向一些路由到 API 服务器而不是到 esbuild
  • 使用自签名证书添加 HTTPS 支持

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

Sourcemap

Supported by: Transform | Build

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

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

启用 source map 将会伴随着任何一个生成的 .js 文件生成一个 .js.map 文件,并且在 .js 文件底部添加特殊的 //# sourceMappingURL= 注释以指向 .js.map 文件。 生成 source map 的四种方式:

  1. linked
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

这种方式同样会生成一个 .js.map 文件,且会和 .js 文件在同一目录,不需要包含 //# sourceMappingURL= 注释。

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
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 配置项。

如果你想同时设置 inline 与 external 的话,你应该设置 source map 模式为 both:

  1. both
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

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

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

在 node 环境中,source map 在 version v12.12.0 版本之后原生支持。 此特性是默认关闭的,但是可以通过一个标志启用。

node --enable-source-maps app.js

Splitting

注意

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

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

  • 多个入口点之间共享的代码可以缓存访问。
  • 通过异步 import() 表达式引用的代码将被分割到一个单独的文件中, 仅在求值该表达式时才加载,减少启动下载时间。

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

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 代码的目标环境,可以设置为类似于 es2020 的 JavaScript 语言版本,或者一个引擎列表(目前可以是 chrome、firefox、safari、edge 或者 node)。默认的 target 为 esnext,这意味着默认情况下,esbuild 将假设所有最新的 JavaScript 特性都是受支持的。

也可以更精确地描述版本号 (例如 设置为 node12.19.0 而不仅仅是 node12):

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

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

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

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

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

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

Watch

Supported by: Build

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

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

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

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

如果要终止 watch ,可以使用 stop 方法。

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

  setTimeout(() => {
    result.stop()
    console.log('stopped watching')
  }, 10 * 1000)
})

result := api.Build(api.BuildOptions{
  EntryPoints: []string{"app.js"},
  Outfile:     "out.js",
  Bundle:      true,
  Watch:       &api.WatchMode{},
})
fmt.Printf("watching...\n")

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

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

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

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

Write

Supported by: Build

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

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

启用此设置允许输出文件覆盖输入文件。默认情况下不启用它,因为这样做意味着覆盖源代码,如果未签入代码,可能会导致数据丢失。但支持这一点可以避免使用临时目录,从而简化某些工作流。因此,当您想要故意覆盖源代码时,可以启用此选项:

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

使用 Analyze 功能可以生成一份关于 bundle 内容的易于阅读的报告:

esbuild --bundle example.jsx --outfile=out.js --minify --analyze
(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)
}

这些信息显示了每个输出文件中的输入文件以及它们最终占用的输出文件的百分比。如果您想了解更多信息,可以启用 verbose 模式。这当前显示了从入口点到每个输入文件的导入路径,它告诉您为什么包中包含给定的输入文件:

esbuild --bundle example.jsx --outfile=out.js --minify --analyze=verbose
(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)
}

这种分析只是 metafile 中直观的信息。如果此分析不完全符合您的需要,欢迎您使用 metafile 中的信息构建自己的显示。

请注意,此格式化的分析摘要适用于人类,而不是机器。具体的格式可能会随着时间的推移而改变,这可能会破坏任何试图解析它的工具。您不应该编写工具来解析这些数据。您应该使用JSON元数据文件中的信息。此可视化中的所有内容都源自JSON元数据,因此您不会因为不解析esbuild的格式化分析摘要而丢失任何信息。

Asset names

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

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

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

  • [dir]

    这是基于 outbase 目录的相对路径。

  • [name]

    这是不带拓展名的原始资源文件名称。例如,如果一个资源原来名为 image.png,然后模板中的 [name] 就会被 image 替换。没有必要使用该占位符;它的存在只是为了提供对人类友好的资源 名称,使调试更容易。

  • [hash]

    这是资源的内容哈希,可以避免命名冲突。例如,你的代码可能会导入 components/button/icon.png 和 components/select/icon.png ,在这种情况下, 你需要使用哈希值来区分这两个都命名为 icon 的资源。

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

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

Supported by: Transform | Build

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

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, 你可以通过设置字符集来禁用字符转义:

echo 'let π = Math.PI' | esbuild
# let \u03C0 = Math.PI;
echo 'let π = Math.PI' | esbuild --charset=utf8
# let π = 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)
  }
}

一些警告:

  • 这还不会转义嵌入在正则表达式中的非 ASCII 字符。这是因为 esbuild 目前根本不解析正则表达式的内容。 尽管有这个限制,但还是添加了这个标志,因为它对于不包含这种情况的代码仍然有用。
  • 此标志不适用于注释。我认为在注释中保留非 ASCII 数据应该没有问题,因为即使编码是错误的, 运行时环境也应该完全忽略所有注释的内容。例如,V8 的博文open in new window 提到了一种优化,可以完全避免对评论内容进行解码。esbuild 会剔除除与许可相关的所有注释。
  • 此选项同时适用于所有输出文件类型(JavaScript、CSS 和 JSON)。因此,如果你配置你的 web 服务器发送正确的 Content-Type 头,并希望使用 UTF-8 字符集, 请确保你的 web 服务器配置为将 .js 和 .css文件都作为 UTF-8 处理。

Chunk 名称

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

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 路径模板中有两个可用占位符:

  • [name]

目前这将始终是 chunk,尽管这个占位符在将来的版本中可能会有额外的值。

  • [hash]

这是 chunk 的内容哈希。在生成多个共享代码块的情况下,内容哈希是区分不同 chunk 的必要条件。

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

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

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

颜色

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

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

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

1 warning and 1 error

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

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 目前只认可了推荐使用的 development 和 production 定制条件。下面是一个添加自定义条件 custom1 和 custom2 的示例:

esbuild src/app.js --bundle --conditions=custom1,custom2
require('esbuild').buildSync({
  entryPoints: ['src/app.js'],
  bundle: true,
  conditions: ['custom1', 'custom2'],
})
package main

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

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

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

conditions 是如何工作的

Conditions 允许你在不同的情况下将相同的导入路径重定向到不同的文件位置。 包含条件和路径的重定向映射存储在包的 package.json 的 exports 字段中。 例如,在使用 import 与 require 条件下会重新映射 require('pkg/foo') 到 pkg/required.cjs, import '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 有五种内置特定行为,并且不能被禁用:

  • default

    该条件总处于激活状态。它的目的是放在最后,让你在没有其他条件应用时提供兜底。

  • import

    该条件仅在通过 ESM import 声明或者 import() 表达式导入路径时生效。 可以用来提供特定于 ESM 的代码。

  • require

    该条件仅在通过 CommonJS require() 调用导入路径时生效。 可以用来提供特定于 CommonJS 的代码。

  • browser

    该条件仅在 esbuild 的 platform 设置为 browser 有效。 可以用来提供特定于浏览器的代码。

  • node

    该条件仅在 esbuild 的 platform 设置为 node 有效。 可以用来提供特定于 node 的代码。

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

入口名称

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

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

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

  • [dir]

    这是从包含输入入口点文件的目录到 outbase 目录的相对路径。 它的目的是帮助你避免不同子目录中命名相同的入口点之间的冲突。

    例如,如果有两个入口点 src/pages/home/index.ts 与 src/pages/about/index.ts,outbase 文件夹为 src, 入口名称模板为 [dir]/[name],输出文件夹将会包含 pages/home/index.js 与 pages/about/index.js。如果入口名称模板仅被设置为 [name],打包将会失败, 因为在输出文件夹中包含两个相同输出路径的输出文件。

  • [name]

    这是不带拓展名的原始入口文件名称。例如,如果入口文件名为 app.js,那么模板中的 [name] 将会被 替换为 app。

  • [hash]

    这是输出文件的内容哈希,可以用来最大化利用浏览器缓存。在你的入口点名称中添加 [hash], 这就意味着 esbuild 会计算与相应输出文件有关系的所有内容的哈希值(如果 代码分隔 激活状态,也包括他导入的任何输出文件)。当且仅当与该输出文件相关的任何输入文件被更改时哈希值才会发生变化。

    之后,你可以让 web 服务器告诉浏览器永久缓存这些文件(你可以说它们从现在起过期很长一段时间,比如一年)。 你可以使用 metafileopen in new window 中的信息来确定哪个输出文件路径对应于哪个输入入口点,这样你就知道要在 <script> 标签中引入哪个路径。

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

    该配置项与 asset nameschunk names 相似。

    Supported by: Transform | Build

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

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(代表立即执行函数表达式)时有效。 它设置了全局变量的名称,用于存储从入口点导出的文件:

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

由于JavaScript是一种动态语言,识别未使用的代码有时对编译器来说非常困难,因此社区开发了一些注释,以帮助告诉编译器哪些代码应该被认为是无副作用的,可以删除。目前,esbuild支持两种形式的副作用注释:

函数调用之前的Inline /*@__PURE__*/ 注释告诉esbuild,如果不使用结果值,可以删除函数调用。有关更多信息,请参阅纯API选项。

在package.json 中的 sideEffects 字段可以用来告诉 esbuild,如果从包中导入的所有文件最终都未被使用,那么你的包中的文件可以被删除,这是 Webpack 的惯例,许多发布到npm的库在其包定义中已经有了这个字段。你可以从 Webpack 的文档open in new window中了解关于这个字段的更多信息。

这些注释可能会有问题,因为编译器的准确性完全依赖于开发人员,开发人员有时会发布带有错误注释的包。sideEffects字段对于开发人员来说特别容易出错,因为默认情况下,如果不使用导入,它会导致包中的所有文件都被视为死代码。如果你添加了一个包含副作用的新文件,却忘了更新该字段,那么当人们试图bundle它时,你的包可能会被破坏。

esbuild app.js --bundle --ignore-annotations
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  ignoreAnnotations: true,
  outfile: 'out.js',
})
package main

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

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

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

启用此选项意味着 esbuild 将不再依赖 /* @__PURE__ */ 或 sideEffects 字段。不过,它仍然会对未使用的导入进行自动 tre shaking。这只是一个临时解决方案,你应该将这些问题报告给软件包的维护人员,以使其得到修复。

增量

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

  • 文件存储在内存中,如果文件 metadata 自上次构建以来没有更改,则不会从文件系统中重新读取文件。 此优化仅适用于文件系统路径。它不适用于由 插件 创建的虚拟模块。
  • 解析后的 ASTs 存储在内存中, 如果文件内容自上次构建以来没有更改,则可以避免重新解析 AST。 除了文件系统模块之外,这个优化还适用于插件创建的虚拟模块,只要虚拟模块路径保持不变。

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

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

这个选项告诉 esbuild 如何处理 JSX 语法。你可以让 esbuild 将 JSX 转换为 JS(默认设置),也可以在输出中保留 JSX 语法,要保留 JSX 语法,请执行以下操作:

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

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

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 这样的库中使用):

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 值,甚至是在压缩的代码中:

esbuild app.js --minify --keep-names
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  minify: true,
  keepNames: true,
  outfile: 'out.js',
})
package main

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

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

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

Supported by: Transform | Build

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

  • none Do not preserve any legal comments.

  • inline Preserve all legal comments.

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

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

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

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

esbuild app.js --legal-comments=eof
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  legalComments: 'eof',
})
package main

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

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

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

日志级别

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

  • silent Do not show any log output.

  • error Only show errors.

  • warning Only show warnings and errors.

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

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

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

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

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 将其完全禁用。这会显示所有日志消息:

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 字段会决定导入哪个文件 (还有 很多其他的规则open in new window)。 包括 esbuild 在内的主流打包器允许你在解析包是额外指定一个 package.json 字段。 通常至少有三个这样的字段:

  • main

    这是对于所有用于 node 中的包的 标准字段open in new window。 main 这个名字被硬编码到 node 的模块解析逻辑中。因为它是用于与 node 一起使用的, 所以可以合理地预期该字段中的文件路径是 commonjs 风格的模块。

  • module

    这个字段来自于一个关于如何将 ECMAScript 模块集成到 node 中的提案。 正因如此,可以合理地预期该字段中的文件路径是 ECMAScript 风格的模块。该提案没有被 node 接收(node 使用 "type": "module"), 但是它被主流打包器采纳,因为 ECMAScript 风格的模块可以更好的 tree shaking 或者无用代码移除。

    对包的作者:一些包错误的将 module 字段设置为了特定于浏览器的代码,main 字段的是 特定于 node 端的代码。很有可能因为 node 忽略了 module 字段并且人们通常只对特定于 浏览器的代码使用打包器。然而,打包特定于 node 的代码也很有价值(例如 它减少了下载和引导时间), 并且那些把特定于浏览器的代码放到 module 中的包,会使捆绑器无法有效地进行 tree shaking 操作。 如果你正在尝试发布特定于浏览器的代码,请使用 browser 字段。

  • browser

    这个字段来自于一个提案open in new window, 该提案允许打包器替换特定于 node 端的文件或者模块为他们的浏览器友好版本 它允许你指定另一个特定于浏览器的入口点。 请注意,包可以同时使用 browser 和 module 字段(见下面的说明)。

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

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

Metafile

Supported by: Build

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

esbuild app.js --bundle --metafile=meta.json --outfile=out.js
const result = require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  metafile: true,
  outfile: 'out.js',
})
require('fs').writeFileSync('meta.json',
  JSON.stringify(result.metafile))
package main

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

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

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

  ioutil.WriteFile("meta.json", []byte(result.Metafile), 0644)
}

该数据可以被其他工具分析。例如,bundle buddyopen in new window 可以消费 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_PATHopen in new window 的环境变量,该变量包含在解析导入路径时使用的全局目录列表。除了所有父目录中的 node_modules 目录之外, 还会在这些路径中搜索包。你可以在 CLI 中使用环境变量,在 JS 和 Go api 中使用数组将这个目录列表传递给 esbuild:

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。你可以像这样使用它:

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.ts 与 src/pages/about/index.ts 两个入口点,并且 outbase 目录为 src, 输出目录将会包含 pages/home/index.js 与 pages/about/index.js。 下面是如何使用它:

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 文件夹,它默认为所有入口起点路径的 最低共有祖先open in new window 目录。 上面的例子是 src/pages,这意味着输出目录将会包含 home/index.js 与 about/index.js。

Supported by: Build

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

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 linkopen in new window, 它指的是一种文件系统特性,其中路径可以重定向到另一个路径。

Public path

Supported by: Build

这与 external file loader 结合会很有用。 默认情况下,loader 使用 default 导出将导入文件的名称导出为字符串。public path 配置项 允许你在这个 loader 加载的每个文件的导出字符串前添加一个基本路径:

esbuild app.js --bundle --loader:.png=file --public-path=https://www.example.com/v1 --outdir=out
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  loader: { '.png': 'file' },
  publicPath: 'https://www.example.com/v1',
  outdir: 'out',
})
package main

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

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

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

Pure

Supported by: Transform | Build

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

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

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

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

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

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

echo 'console.log("foo:", foo())' | esbuild --pure:console.log
# /* @__PURE__ */ console.log("foo:", foo());
echo 'console.log("foo:", foo())' | esbuild --pure:console.log --minify
# foo();
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

esbuild app.js --bundle --resolve-extensions=.ts,.js
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  resolveExtensions: ['.ts', '.js'],
  outfile: 'out.js',
})
package main

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

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

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

Source Root

Supported by: Transform | Build

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

你可以像这样配置 sourceRoot:

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>。 你可以像这样配置:

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 格式的 第三版open in new window 生成 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 更小:

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 没有指定解析目录的方法。相反,它被自动设置为当前工作目录。

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

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 模块 import和 export 语句。 它不能与 CommonJS 模块一起工作。npm 上的许多库都包含了这两种格式,esbuild 会在默认情况下选择适合 tree shaking 的格式。你可以使用 main fields 配置项自定义 esbuild 选择的格式。

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

  • 函数调用前的行内 /* @__PURE__ */ 注释告诉 esbuild 该函数调用如果在结果没有被使用的情况下可以被移除。 查看 pure API 配置项获取更多信息。

  • package.json 中的 sideEffects 字段也可以用来告诉 esbuild 在你的包中的哪些文件 在始终没有使用的情况下可以被移除。这是一个来自 webpack 的公约,并且很多发布到 npm 的库已经在 其包定义中包含此字段。你可以在 Webpack 的文档open in new window 中了解到更多关于该字段的信息。

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

所以 esbuild 包含一种忽略 tree-shaking 注释的方法。只有当你遇到一个问题, bundle 因为意外地从 bundle 中删除了必要的代码而破坏时,你才应该启用这个功能。

esbuild app.js --tree-shaking=true
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  treeShaking: true,
  outfile: 'out.js',
})
package main

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

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

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

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

esbuild app.js --tree-shaking=false
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  treeShaking: false,
  outfile: 'out.js',
})
package main

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

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

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

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

Tsconfig

Supported by: Build

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

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, 其不会访问文件系统。像这样使用它:

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 配置给出的相对路径解析为绝对路径, 以及将绝对路径解析为日志消息中的相对路径。下面是如何覆盖它:

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

在浏览器中运行

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>