API
可以通过如下三种方式调用 API:在命令行中调用,在 JavaScript 中调用, 在 Go 中调用。 概念和参数在这三种方式中基本相同,因此本文档将三种方式的 API 文档合并显示, 不再按照不同的使用方式来拆分文档。
在 esbuild 的 API 中有两种主要的 API 调用:transform 与 build。理解使用哪一个 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 示例:
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 可以使用以下配置项:
一般配置项:
高级配置:
- Banner
- Charset
- Drop
- Footer
- Ignore annotations
- JSX
- JSX factory
- JSX fragment
- Keep names
- Legal comments
- Mangle props
- Pure
- Source root
- Sourcefile
- Sources content
- Tree shaking
- Tsconfig raw
JS 特殊细节:
# Build API
调用 build API 操作文件系统中的一个或多个文件。 它允许文件互相引用并且打包在一起。 这里是一个简单的 build 用例:
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 可以使用以下配置项:
一般配置项:
- Bundle
- Define
- Entry points
- External
- Format
- Inject
- Loader
- Minify
- Outdir
- Outfile
- Platform
- Serve
- Sourcemap
- Target
- Watch
- Write
高级配置:
- Allow overwrite
- Analyze
- Banner
- Charset
- Conditions
- Drop
- Footer
- Ignore annotations
- JSX
- JSX factory
- JSX fragment
- Keep names
- Legal comments
- Main fields
- Mangle props
- Metafile
- Node paths
- Out extension
- Outbase
- Preserve symlinks
- Public path
- Pure
- Resolve extensions
- Source root
- Sourcefile
- Sources content
- Stdin
- Tree shaking
- Tsconfig
- Working directory
JS 特殊细节:
# 一般配置项
# Bundle
Supported by: Build
打包一个文件意味着将任何导入的依赖项内联到文件中。 这个过程是递归的,因为依赖的依赖(等等)也将被内联。 默认情况下,esbuild 将 不会 打包输入的文件。 打包必须想这样显式启用:
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
该特性提供了一种用常量表达式替换全局标识符的方法。 它可以在不改变代码本身的情况下改变某些构建之间代码的行为:
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的替换值本身必须包含引号。 省略引号意味着替换的值是一个标识符:
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:
esbuild home.ts settings.ts --bundle --outdir=out
require('esbuild').buildSync({
entryPoints: ['home.ts', 'settings.ts'],
bundle: true,
write: true,
outdir: 'out',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"home.ts", "settings.ts"},
Bundle: true,
Write: true,
Outdir: "out",
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
This will generate two output files, out/home.js
and out/settings.js
corresponding to the two entry points home.ts
and settings.ts
.
For further control over how the paths of the output files are derived from the corresponding input entry points, you should look into these options:
In addition, you can also specify a fully custom output path for each individual entry point using an alternative entry point syntax:
esbuild out1=home.js out2=settings.js --bundle --outdir=out
require('esbuild').buildSync({
entryPoints: {
out1: 'home.js',
out2: 'settings.js',
},
bundle: true,
write: true,
outdir: 'out',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPointsAdvanced: []api.EntryPoint{{
OutputPath: "out1",
InputPath: "home.js",
}, {
OutputPath: "out2",
InputPath: "settings.js",
}},
Bundle: true,
Write: true,
Outdir: "out",
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
This will generate two output files, out/out1.js
and out/out2.js
corresponding to the two entry points home.ts
and settings.ts
.
# External
Supported by: Build
你可以标记一个文件或者包为外部(external),从而将其从你的打包结果中移除。 导入将被保留(对于 iife
以及 cjs
格式使用 require
,对于 esm
格式使用 import
),而不是被打包, 并将在运行时进行计算。
这里有几个用法。首先,它可以用于去除你的 bundle 中你知道将永远不会被执行的代码路径中的无用代码。 例如,一个 package 可以会包含值运行在 node 端的代码,但是你只会将其用在浏览器中。 它还可以用于在运行时从不能打包的包导入 node 中的代码。例如,fsevents
包含 esbuild 不支持的本地拓展, 像这样将某些内容标记为外部(external):
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:
Before path resolution begins, import paths are checked against all external paths. In addition, if the external path looks like a package path (i.e. doesn't start with
/
or./
or../
), import paths are checked to see if they have that package path as a path prefix.This means that
--external:
implicitly also means@foo/ bar --external:
which matches the import path@foo/ bar/* @foo/
. So it marks all paths inside thebar/ baz @foo/bar
package as external too.After path resolution ends, the resolved absolute paths are checked against all external paths that don't look like a package path (i.e. those that start with
/
or./
or../
). But before checking, the external path is joined with the current working directory and then normalized, becoming an absolute path (even if it contains a*
wildcard character).This means that you can mark everything in the directory
dir
as external using--external:
. Note that the leading./dir/* ./
is important. Using--external:
instead is treated as a package path and is not checked for after path resolution ends.dir/*
# 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...
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"。它假设环境支持 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
加载模块。 请注意 node 需要--experimental- modules file.mjs .mjs
拓展名,除非你在package.json
文件中配置了"type":
。 你可以使用 esbuild 中的 out extension 设置来自定义生成文件的拓展名。 你可以点击 这里 获取更多关于使用 ECMAScript modules 的内容。"module"
# 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())
You can map process.cwd
to dummy_process_cwd
with the define feature, then inject dummy_process_cwd
from process-shim.js
with the inject feature:
esbuild entry.js --bundle --define:process.cwd=dummy_process_cwd --inject:./process-shim.js --outfile=out.js
require('esbuild').buildSync({
entryPoints: ['entry.js'],
bundle: true,
define: { 'process.cwd': 'dummy_process_cwd' },
inject: ['./process-shim.js'],
outfile: 'out.js',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"entry.js"},
Bundle: true,
Define: map[string]string{
"process.cwd": "dummy_process_cwd",
},
Inject: []string{"./process-shim.js"},
Outfile: "out.js",
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
结果如下:
// out.js
function dummy_process_cwd() {
return "";
}
console.log(dummy_process_cwd());
# 自动导入 JSX
你可以使用 inject 特性以自动提供 JSX 表达式的执行环境。例如,你可以自动导入 react
包以执行诸如 React.createElement
的函数。查看 JSX 文档 获取更多。
# 不使用 import 注入文件
你可以对不包含 exports 的文件使用该特性。在这种情况下,注入的文件就像每个输入文件都包含 import
一样出现在输出的前面。 由于 ECMAScript 模块的工作方式,这个注入仍然是 “卫生的”,因为在不同文件中具有相同名称的符号会被重命名, 这样它们就不会相互冲突。
# 选择性注入文件
如果你仅想当导出被实际使用的情况下 有条件 的引入一个文件,你应该通过将其置于 package 中并且 在 package.json
中添加 "sideEffects":
以标记被注入的文件没有副作用。 该设置为 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 调用进行打包,就像这样:
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=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 中这样启用压缩:
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)
}
}
该配置项结合起来做三件独立的事情:移除空格、重写语法使其更体积更小、重命名变量为更短的名称。 一般情况下这三件事情你都想做,但是如果有必要的话,这些配置项可以单独启用:
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:
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 作为压缩器时需要记住以下几点:
当启用压缩时也可能也需要设置 target 配置项 。默认情况下, esbuild 利用现代 JavaScript 特性使你的代码变得更小。例如,
a ===
可以被压缩为undefined || a === null ? 1 : a a ?? 1
。如果你不想让 esbuild 在压缩是利用现代 JavaScript 特性, 你应该使用一个更老的语言目标,例如--target=es6
。The character escape sequence
\n
will be replaced with a newline character in JavaScript template literals. String literals will also be converted into template literals if the target supports them and if doing so would result in smaller output. This is not a bug. Minification means you are asking for smaller output, and the escape sequence\n
takes two bytes while the newline character takes one byte.By default esbuild won't minify the names of top-level declarations. This is because esbuild doesn't know what you will be doing with the output. You might be injecting the minified code into the middle of some other code, in which case minifying top-level declaration names would be unsafe. Setting an output format (or enabling bundling, which picks an output format for you if you haven't set one) tells esbuild that the output will be run within its own scope, which means it's then safe to minify top-level declaration names.
对于所有的 JavaScript 代码, 压缩不是 100% 安全的。 这对 esbuild 和其他流行的 JavaScript 压缩器, 例如 terser 是真实存在的。 特别地,esbuild 并不是为了保存函数上调用
- >.toString()
的值而设计的。 原因是如果所有函数中的所有代码都要逐字保存的话, 压缩会变得非常困难并且实际上是没有用的。 然而,这意味着依赖于.toString()
返回值的 JavaScript 代码 在压缩过程中可能会中断。 例如,在 AngularJS 框架中一些模式在代码压缩时会中断, 因为 Angular 使用.toString()
来读取函数的参数名称。 一个解决方案是使用 明确的注释替代。默认情况下,esbuild 不会在函数和类对象上保留
- |.name
的值。这是因为大多数代码不会依赖该属性, 并且使用更短的名称是一个重要的大小优化。然而,一些代码确实依赖.name
属性来注册和绑定。 如果你需要依赖该属性,你应该启用 keep names 配置项。使用某些 JavaScript 特性可以禁用 esbuild 的许多优化,包括压缩。 具体来说,直接使用
eval
和/或with
语句可以阻止 esbuild 将标识符重命名为更小的名称, 因为这些特性会导致标识符绑定在运行时而不是编译时发生。 这几乎总是无意的,而且只发生在人们不知道什么是直接 eval 以及它为什么不好的情况下。如果你正在考虑像这样写一段代码:
// 直接使用 eval (将会禁用整个文件的压缩) let result = eval(something)
你应该这样写,你的代码才能被压缩:
// 间接使用 eval (对周围的代码没有副作用) let result = (0, eval)(something)
这里 可以获取关于直接使用
- >eval
以及可用选择的更多信息。esbuild 中的压缩算法还没有进行高级代码优化。特别是,下面的代码优化对 JavaScript 是 有可能的,但不是由 esbuild 完成的(不是一个详尽的列表):
- 消除函数体中的 dead-code
- 函数内联
- Cross-statement 常数传播
- 对象形状建模
- Allocation sinking
- Method devirtualization
- Symbolic execution
- JSX 表达式提升
- TypeScript 枚举检测和内联
如果你的代码使用的模式要求某些形式的代码优化是紧凑的,或者如果你正在为你的用例搜索最佳的 JavaScript 压缩算法,你应该考虑使用其他工具。实现这些高级代码优化的一些工具示例包括 Terser 和 Google Closure Compiler。
# 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 之前清除输出文件夹。
如果你的构建有多个入口,且多个入口在单独的文件夹内,目录结构将从所有输入入口点路径中 最低的公共祖先 目录开始复制到输出目录中。If your build contains multiple entry points in separate directories, the 例如,这里有两个入口起点 src/
与 src/
,输出文件夹将会包含 home/
与 about/
。 如果你想要自定义该行为,你应该改变 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-browserify
替换path
。main fields 设置为
browser,
, 但是会有一些额外的特殊行为。如果你个包支持module, main module
与main
,但是不支持browser
, 那么当使用require()
导入时,将使用main
而不是module
。 此行为通过将函数赋值给module.exports
来改善与导出函数的 CommonJS 模块的兼容性。conditions 设置自动包含了
browser
情况。 这将改变package.json
文件中exports
字段如何被解释为偏好特定于浏览器代码的方式。All
process.
expressions are automatically defined toenv. NODE_ENV "production"
if all minification options are enabled and"development"
otherwise. This only happens ifprocess
,process.env
, andprocess.env.NODE_ENV
are not already defined. This substitution is necessary to avoid React-based code crashing instantly (sinceprocess
is a node API, not a web API).
当 platform 设置为 node
时:
默认输出 格式 为
cjs
,代表 CommonJS(node 使用的模块格式)。 ES6-风格的导出使用的export
语句将会被转换为 CommonJSexports
对象中的 getters。所有诸如
fs
的 内置 node 模块 会被自动标记为 external,因此在打包器尝试打包他们时不会导致错误。main 字段 设置为
main,
。 这意味着 tree shaking 操作可能不会发生在同时提供module 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 设置不会自动包含任何平台特定值。
# 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/
的文件,web 服务器将会有一个相应的 /pages/
路径。
如果你想要浏览 web 服务器以查看哪些 URL 是有效的,你可以通过访问文件夹名称而不是文件名称来使用内置文件夹列表。 例如,如果你正在 8000 端口运行 esbuild 的 web 服务器,你可以在浏览器中访问 http://
# 参数
请注意 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
文件。 There are four different modes for source map generation:
-
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. Uselinked
source map mode like this:
-
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. Uselinked
source map mode like this:
esbuild app.ts --sourcemap --outfile=out.js
require('esbuild').buildSync({
entryPoints: ['app.ts'],
sourcemap: true,
outfile: 'out.js',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.ts"},
Sourcemap: api.SourceMapLinked,
Outfile: "out.js",
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
如果输入的文件本身包含特殊 //# sourceMappingURL=
注释,esbuild 将会自动尝试解析 链接的 source map。如果成功的话,生成的源映射中的映射将一路映射回输入源映射中引用的原始源代码。
-
external
This mode means the source map is generated into a separate
.js.map
output file alongside the.js
output file, but unlikelinked
mode the.js
output file does not contain a//# sourceMappingURL=
comment. Useexternal
source map mode like this:
-
external
This mode means the source map is generated into a separate
.js.map
output file alongside the.js
output file, but unlikelinked
mode the.js
output file does not contain a//# sourceMappingURL=
comment. Useexternal
source map mode like this:
esbuild app.ts --sourcemap=external --outfile=out.js
require('esbuild').buildSync({
entryPoints: ['app.ts'],
sourcemap: 'external',
outfile: 'out.js',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.ts"},
Sourcemap: api.SourceMapExternal,
Outfile: "out.js",
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
如果你想插入整个 source map 到 .js
文件中而不是单独生成一个 .js.map
文件,你应该设置 source map 模式为 inline
:
-
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 containinginline
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. Useinline
source map mode like this:
esbuild app.ts --sourcemap=inline --outfile=out.js
require('esbuild').buildSync({
entryPoints: ['app.ts'],
sourcemap: 'inline',
outfile: 'out.js',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.ts"},
Sourcemap: api.SourceMapInline,
Outfile: "out.js",
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
请记住 source map 通常是比较大的,因为他们包含所有的源代码,因此你通常不会想让代码中包含 inline
source maps。为了移除 source map 中的源代码(只保存文件名以及行/列映射关系), 请使用 sources content 配置项。
如果你想同时设置 inline
与 external
的话,你应该设置 source map 模式为 both
:
-
both
This mode is a combination of
inline
andexternal
. 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. Useboth
source map mode like this:
esbuild app.ts --sourcemap=both --outfile=out.js
require('esbuild').buildSync({
entryPoints: ['app.ts'],
sourcemap: 'both',
outfile: 'out.js',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.ts"},
Sourcemap: api.SourceMapInlineAndExternal,
Outfile: "out.js",
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
# 使用 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.
。 你代码中的堆栈仍然会给出包含已编译代码的未映射堆栈跟踪。 这里是如何启用浏览器开发者工具中 source map 设置的方式:
- Chrome: ⚙ → Enable JavaScript source maps
- Safari: ⚙ → Sources → Enable source maps
- Firefox: ··· → Enable Source Maps
在 node 环境中,source map 在 version v12.12.0 版本之后原生支持。 此特性是默认关闭的,但是可以通过一个标志启用。不向在浏览器中,实际的堆栈跟踪也被修改了,所以检查你代码中的 error.
将会给出包含源代码的堆栈跟踪。这是如何在 node 中启用该设置(--enable-
标志 必须在文件名之前):
node --enable-source-maps app.js
# 代码分割
启用 "代码分隔" 有两个目的:
多个入口点之间共享的代码被分割成两个入口点都导入的单独共享文件。 这样,如果用户先浏览一个页面,然后再浏览另一个页面,如果共享部分已经被浏览器下载并缓存, 他们就不必从头下载第二个页面的所有 JavaScript。
通过异步
import()
表达式引用的代码将被分割到一个单独的文件中, 仅在求值该表达式时才加载。这允许你通过在启动时只下载你需要的代码来改善应用程序的初始下载时间, 然后在以后需要时延迟下载额外的代码。如果不启用代码分割,
import()
表达式会变成Promise
。 这仍然保留了表达式的异步语义,但这意味着导入的代码包含在同一个包中,而不是分离到单独的文件中。.resolve() .then(() => require())
当期启用代码分割时,你必须使用 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 代码的目标环境。例如,你可以配置 esbuild 不要生成任何 v10 版本的 node 不能处理的新的 JavaScript 代码。target 可以设置为类似于 es2020
的 JavaScript 语言版本,或者一个引擎列表(目前可以是 chrome
、firefox
、safari
、edge
或者 node
)。默认的 target 为 esnext
,这意味着默认情况下,esbuild 将假设所有最新的 JavaScript 特性都是受支持的。
这里是在 esbuild 中使用所有可用 target 环境名称的例子。请注意你不需要指定所有这些; 你可以仅仅指定你的项目关心的目标环境的子集。如果你愿意的话,你也可以更精确地描述版本号 (例如 设置为 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 loader 获取更多关于哪个 语言版本引入了哪些语法特性。请记住尽管像 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...')
})
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,你可以选择性地提供一个回调函数,该函数将会在 增量构建完成后调用。一旦构建完成,就可以使用它来做一些事情(例如 重新加载浏览器中的应用):
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:
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 不是。使用内存缓冲区:
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:
esbuild app.js --outdir=. --allow-overwrite
require('esbuild').buildSync({
entryPoints: ['app.js'],
outdir: '.',
allowOverwrite: true,
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Outdir: ".",
AllowOverwrite: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
# Analyze
Supported by: Build
Using the analyze feature generates an easy-to-read report about the contents of your bundle:
esbuild --bundle example.jsx --outfile=out.js --minify --analyze... out.js 27.4kb 100.0% ├ node_modules/react-dom/cjs/react-dom-server.browser.production.min.js 19.2kb 70.1% ├ node_modules/react/cjs/react.production.min.js 5.9kb 21.5% ├ node_modules/object-assign/index.js 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:
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:
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:
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/
的资源名 称模板,将所有资源放入输出目录内名为 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)
}
}
在资源路径模板中有4个可用占位符:
[dir]
This is the relative path from the directory containing the asset file to the outbase directory. Its purpose is to help asset output paths look more aesthetically pleasing by mirroring the input directory structure inside of the output directory.
[name]
这是不带拓展名的原始资源文件名称。例如,如果一个资源原来名为
image.png
,然后模板中的[name]
就会被image
替换。没有必要使用该占位符;它的存在只是为了提供对人类友好的资源 名称,使调试更容易。[hash]
这是资源的内容哈希,可以避免命名冲突。例如,你的代码可能会导入
components/
和button/ icon.png components/
,在这种情况下, 你需要使用哈希值来区分这两个都命名为 icon 的资源。select/ icon.png [ext]
This is the file extension of the asset (i.e. everything after the end of the last
.
character). It can be used to put different types of assets into different directories. For example,--asset-names=
might write out an asset namedassets/ [ext]/ [name]-[hash] image.png
asassets/
.png/ image-CQFGD2NG.png
资源路径模板不需要包含文件拓展名。资源的原始拓展名将会在模板替换完成后添加到输出路径尾部。
# Banner
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
,或者为他提供正确的 Content-
头,以便浏览器不会损坏代码。另一个原因是,非 ASCII 字符会显著 降低浏览器解析器的速度。 然而,使用转义序列会使生成的输出稍微大一些,也会使其更难阅读。
如果你想让 esbuild 在不使用转义序列的情况下打印原始字符,并且你已经确保浏览器将你的代码解释为 UTF-8, 你可以通过设置字符集来禁用字符转义:
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)
}
}
一些警告:
这还不会转义嵌入在正则表达式中的非 ASCII 字符。这是因为 esbuild 目前根本不解析正则表达式的内容。 尽管有这个限制,但还是添加了这个标志,因为它对于不包含这种情况的代码仍然有用。
此标志不适用于注释。我认为在注释中保留非 ASCII 数据应该没有问题,因为即使编码是错误的, 运行时环境也应该完全忽略所有注释的内容。例如,V8 的博文 提到了一种优化,可以完全避免对评论内容进行解码。esbuild 会剔除除与许可相关的所有注释。
此选项同时适用于所有输出文件类型(JavaScript、CSS 和 JSON)。因此,如果你配置你的 web 服务器发送正确的
Content-
头,并希望使用 UTF-8 字符集, 请确保你的 web 服务器配置为将Type .js
和.css
文件都作为 UTF-8 处理。
# Chunk 名称
此选项控制在启用 代码分割 时自动生成的共享代码块的文件名。 它使用带有占位符的模板来配置输出路径,当生成输出路径时,占位符将被特定于 chunk 的值替换。 例如,指定 chunks/
的 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 路径模板中有3个可用占位符:
[name]
目前这将始终是
chunk
,尽管这个占位符在将来的版本中可能会有额外的值。[hash]
这是 chunk 的内容哈希。在生成多个共享代码块的情况下,内容哈希是区分不同 chunk 的必要条件。
[ext]
This is the file extension of the chunk (i.e. everything after the end of the last
.
character). It can be used to put different types of chunks into different directories. For example,--chunk-names=
might write out a chunk aschunks/ [ext]/ [name]-[hash] chunks/
.css/ chunk-DEFJT7KY.css
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 中,这是很有用的:
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 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:
debugger
Passing this flag causes all
debugger
statements to be removed from the output. This is similar to thedrop_debugger: true
flag available in the popular UglifyJS and Terser JavaScript minifiers.JavaScript's
debugger
statements cause the active debugger to treat the statement as an automatically-configured breakpoint. Code containing this statement will automatically be paused when the debugger is open. If no debugger is open, the statement does nothing. Dropping these statements from your code just prevents the debugger from automatically stopping when your code runs.You can drop
debugger
statements like this:
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)
}
}
console
Passing this flag causes all
console
API calls to be removed from the output. This is similar to thedrop_console: true
flag available in the popular UglifyJS and Terser JavaScript minifiers.WARNING: Using this flag can introduce bugs into your code! This flag removes the entire call expression including all call arguments. If any of those arguments had important side effects, using this flag will change the behavior of your code. Be very careful when using this flag.
If you want to remove console API calls without removing the arguments with side effects (so you do not introduce bugs), you should mark the relevant API calls as pure instead. For example, you can mark
console.log
as pure using--pure:
. This will cause these API calls to be removed safely when minification is enabled.console.log You can drop
console
API calls like this:
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]/
的入口名称模板, 其在文件名中包含输出文件的哈希值,并且将文件置于输出目录中,也可能在子目录下 (查看下面关于 [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)
}
}
在入口路径模板中有4个可用占位符:
[dir]
这是从包含输入入口点文件的目录到 outbase 目录的相对路径。 它的目的是帮助你避免不同子目录中命名相同的入口点之间的冲突。
例如,如果有两个入口点
src/
与pages/ home/ index.ts src/
,outbase 文件夹为pages/ about/ index.ts src
, 入口名称模板为[dir]/[name]
,输出文件夹将会包含pages/
与home/ index.js pages/
。如果入口名称模板仅被设置为about/ index.js [name]
,打包将会失败, 因为在输出文件夹中包含两个相同输出路径的输出文件。[name]
这是不带拓展名的原始入口文件名称。例如,如果入口文件名为
app.js
,那么模板中的[name]
将会被 替换为app
。[hash]
这是输出文件的内容哈希,可以用来最大化利用浏览器缓存。在你的入口点名称中添加
[hash]
, 这就意味着 esbuild 会计算与相应输出文件有关系的所有内容的哈希值(如果 代码分隔 激活状态,也包括他导入的任何输出文件)。当且仅当与该输出文件相关的任何输入文件被更改时哈希值才会发生变化。之后,你可以让 web 服务器告诉浏览器永久缓存这些文件(你可以说它们从现在起过期很长一段时间,比如一年)。 你可以使用 metafile 中的信息来确定哪个输出文件路径对应于哪个输入入口点,这样你就知道要在
<script>
标签中引入哪个路径。[ext]
This is the file extension that the entry point file will be written out to (i.e. the out extension setting, not the original file extension). It can be used to put different types of entry points into different directories. For example,
--entry-names=
might write the output file forentries/ [ext]/ [name] app.ts
toentries/
.js/ app.js
入口路径模板不需要包含一个文件拓展名。根据文件类型,适当的 out 扩展名 将在模板替换 后自动添加到输出路径的末尾。
该配置项与 asset names 和 chunk names 相似。
# Footer
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=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 将生成一个具有该属性的全局变量。 冲突的现有全局变量将不会被覆盖。这可以用来实现“命名空间”,其中多个独立脚本将它们的导出添加到同一个全局对象中。 示例:
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:
Inline
/* @__PURE__ */
comments before function calls tell esbuild that the function call can be removed if the resulting value isn't used. See the pure API option for more information.The
sideEffects
field inpackage.json
can be used to tell esbuild which files in your package can be removed if all imports from that file end up being unused. This is a convention from Webpack and many libraries published to npm already have this field in their package definition. You can learn more about this field in Webpack's documentation for this field.
These annotations can be problematic because the compiler depends completely on developers for accuracy, and developers occasionally publish packages with incorrect annotations. The sideEffects
field is particularly error-prone for developers because by default it causes all files in your package to be considered dead code if no imports are used. If you add a new file containing side effects and forget to update that field, your package will likely break when people try to bundle it.
This is why esbuild includes a way to ignore side-effect annotations. You should only enable this if you encounter a problem where the bundle is broken because necessary code was unexpectedly removed from the bundle:
esbuild app.js --bundle --ignore-annotations
require('esbuild').buildSync({
entryPoints: ['app.js'],
bundle: true,
ignoreAnnotations: true,
outfile: 'out.js',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
IgnoreAnnotations: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
Enabling this means esbuild will no longer respect /* @__PURE__ */
comments or the sideEffects
field. It will still do automatic tree shaking of unused imports, however, since that doesn't rely on annotations from developers. Ideally this flag is only a temporary workaround. You should report these issues to the maintainer of the package to get them fixed since they indicate a problem with the package and they will likely trip up other people too.
# 增量
如果用例使用相同的选项重复调用 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
This option tells esbuild what to do about JSX syntax. You can either have esbuild transform JSX to JS (the default) or preserve the JSX syntax in the output. To preserve JSX syntax:
echo '<div/>' | esbuild --jsx=preserve --loader=jsx<div />;
require('esbuild').transformSync('<div/>', {
jsx: 'preserve',
loader: 'jsx',
}){
code: '<div />;\n',
map: '',
warnings: []
}
package main
import "fmt"
import "github.com/evanw/esbuild/pkg/api"
func main() {
result := api.Transform("<div/>", api.TransformOptions{
JSXMode: api.JSXModePreserve,
Loader: api.LoaderJSX,
})
if len(result.Errors) == 0 {
fmt.Printf("%s", result.Code)
}
}
Note that if you preserve JSX syntax, the output files are no longer valid JavaScript code. This feature is intended to be used when you want to transform the JSX syntax in esbuild's output files by another tool after bundling, usually one with a different JSX-to-JS transform than the one esbuild implements.
# JSX factory
Supported by: Transform | Build
这将设置为每个 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)
}
}
# Legal comments
Supported by: Transform | Build
A "legal comment" is considered to be any statement-level comment in JS or rule-level comment in CSS that contains @license
or @preserve
or that starts with //!
or /*!
. These comments are preserved in output files by default since that follows the intent of the original authors of the code. However, this behavior can be configured by using one of the following options:
none
Do not preserve any legal comments.inline
Preserve all legal comments.eof
Move all legal comments to the end of the file.linked
Move all legal comments to a.LEGAL.txt
file and link to them with a comment.external
Move all legal comments to a.LEGAL.txt
file but to not link to them.
The default behavior is eof
when bundle is enabled and inline
otherwise. Setting the legal comment mode looks like this:
esbuild app.js --legal-comments=eof
require('esbuild').buildSync({
entryPoints: ['app.js'],
legalComments: 'eof',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
LegalComments: api.LegalCommentsEndOfFile,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
Note that "statement-level" for JS and "rule-level" for CSS means the comment must appear in a context where multiple statements or rules are allowed such as in the top-level scope or in a statement or rule block. So comments inside expressions or at the declaration level are not considered license comments.
# 日志级别
可以修改日志级别,以阻止 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 frominfo
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
字段会决定导入哪个文件 (还有 很多其他的规则)。 包括 esbuild 在内的主流打包器允许你在解析包是额外指定一个 package.json
字段。 通常至少有三个这样的字段:
main
这是对于所有用于 node 中的包的 标准字段。
main
这个名字被硬编码到 node 的模块解析逻辑中。因为它是用于与 node 一起使用的, 所以可以合理地预期该字段中的文件路径是 commonjs 风格的模块。module
这个字段来自于一个关于如何将 ECMAScript 模块集成到 node 中的提案。 正因如此,可以合理地预期该字段中的文件路径是 ECMAScript 风格的模块。该提案没有被 node 接收(node 使用
"type":
), 但是它被主流打包器采纳,因为 ECMAScript 风格的模块可以更好的 tree shaking 或者无用代码移除。"module" 对包的作者:一些包错误的将
module
字段设置为了特定于浏览器的代码,main
字段的是 特定于 node 端的代码。很有可能因为 node 忽略了module
字段并且人们通常只对特定于 浏览器的代码使用打包器。然而,打包特定于 node 的代码也很有价值(例如 它减少了下载和引导时间), 并且那些把特定于浏览器的代码放到module
中的包,会使捆绑器无法有效地进行 tree shaking 操作。 如果你正在尝试发布特定于浏览器的代码,请使用browser
字段。browser
这个字段来自于一个提案, 该提案允许打包器替换特定于 node 端的文件或者模块为他们的浏览器友好版本 它允许你指定另一个特定于浏览器的入口点。 请注意,包可以同时使用
browser
和module
字段(见下面的说明)。
默认的 main 字段依赖于当前 platform 设置,本质上是 browser,
(浏览器)与 main,
(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"
}
}
# Mangle props
Supported by: Transform | Build
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({
into print({
:
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({
will be mangled into print({
while print({
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:
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__
:
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:
You can customize what mangled properties are renamed to by editing the cache before passing it to esbuild.
The cache serves as a list of all properties that were mangled. You can easily scan it to see if there are any unexpected property renamings.
You can disable mangling for individual properties by setting the renamed value to
false
instead of to a string. This is similar to the reserve props setting but on a per-property basis.You can ensure consistent renaming between builds (e.g. a main-thread file and a web worker, or a library and a plugin). Without this feature, each build would do an independent renaming operation and the mangled property names likely wouldn't be consistent.
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:
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
的文件中:
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:
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/
与 src/
两个入口点,并且 outbase 目录为 src
, 输出目录将会包含 pages/
与 pages/
。 下面是如何使用它:
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/
,这意味着输出目录将会包含 home/
与 about/
。
# Preserve symlinks
Supported by: Build
这设置与 node 中的 --preserve-symlinks
设置相映射。 如果你使用这个设置(或者是 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 link, 它指的是一种文件系统特性,其中路径可以重定向到另一个路径。
# 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 --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.js
、./file.json
与 ./file.node
。包括 esbuild 在内的现代打包器 将此概念拓展到了其他文件类型。在 esbuild 中可以使用解析插件设置对隐式文件拓展名进行自定义配置, 默认为 .tsx,
:
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
:
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 格式的 第三版 生成 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
打包该文件, 没有使用到的函数将会自动销毁,并为你产生以下输出:
// 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
打包该文件, 没有使用到的函数将会自动销毁,并为你产生以下输出:
// 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 的文档 中了解到更多关于该字段的信息。
这些注释可能会产生问题,因为编译器完全依赖于开发人员来确保准确性,而开发人员偶尔会发布带有 不正确注释的包。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
:
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,因为这不会依赖开发者注释。理想情况下, 这个标志只是一个临时的解决方案。你应该向包的维护者报告这些问题以修复它们, 因为它们表明了包的一个问题,而且它们可能也会使其他人出错。 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
文件。如果你需要对同一份代码针对不同的设置 做多次打包时会非常有用:
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)
}
}
# 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:
- Avoiding promises can result in cleaner code
- Works in situations that must be synchronous such as within
require
.extensions
Cons:
- You can't use plugins with the synchronous API since plugins are asynchronous
- It blocks the current thread so you can't perform other work in the meantime
- Using the synchronous API prevents esbuild from parallelizing esbuild API calls
# Async API
Asynchronous API calls return their results using a promise:
let esbuild = require('esbuild')
esbuild.transform(code, options).then(result => { ... })
esbuild.build(options).then(result => { ... })
# 在浏览器中运行
Pros:
- You can use plugins with the asynchronous API
- The current thread is not blocked so you can perform other work in the meantime
- You can run many simultaneous esbuild API calls concurrently which are then spread across all available CPUs for maximum performance
Cons:
- Using promises can result in messier code, especially in CommonJS where top-level await is not available
- Doesn't work in situations that must be synchronous such as within
require
.extensions
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:
。然后,它会在调用 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>