API
API 可以通过三种语言访问:命令行、JavaScript 和 Go。三种语言的概念和参数基本相同,因此它们将在此处一起介绍,而不是为每种语言提供单独的文档。你可以使用每个代码示例右上角的 CLI
、JS
和 Go
选项卡在语言之间切换。每种语言的一些具体细节
CLI: 如果你使用的是命令行 API,了解标志有三种形式可能会有所帮助:
--foo
、--foo=bar
或--foo:bar
。形式--foo
用于启用布尔标志,例如--minify
,形式--foo=bar
用于具有单个值且仅指定一次的标志,例如--platform=
,形式--foo:bar
用于具有多个值且可以多次重新指定的标志,例如--external:
.JavaScript: 如果你使用的是 JavaScript,请务必查看下面的 JS 特定细节 和 浏览器 部分。你可能还会发现 esbuild 的 TypeScript 类型定义 作为参考很有帮助。
Go: 如果你使用的是 Go,你可能会发现 esbuild 的自动生成的 Go 文档作为参考很有帮助。
pkg/api
和pkg/cli
两个公共 Go 包都有单独的文档:pkg/api
和pkg/cli
.
#概述
esbuild API 中最常用的两个 API 是 build 和 transform。下面将对每个 API 进行高级别的描述,然后是每个 API 选项的文档。
#构建
这是 esbuild 的主要接口。你通常会传递一个或多个 入口点 文件进行处理,以及各种选项,然后 esbuild 将结果写回文件系统。以下是一个简单的示例,它启用了 打包 和 输出目录
esbuild app.ts --bundle --outdir=dist
import * as esbuild from 'esbuild'
let result = await esbuild.build({
entryPoints: ['app.ts'],
bundle: true,
outdir: 'dist',
})
console.log(result)
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.ts"},
Bundle: true,
Outdir: "dist",
})
if len(result.Errors) != 0 {
os.Exit(1)
}
}
构建 API 的高级用法涉及设置一个长期运行的构建上下文。此上下文在 JS 和 Go 中是一个显式对象,但在 CLI 中是隐式的。使用给定上下文完成的所有构建都共享相同的构建选项,并且后续构建是增量完成的(即,它们重用先前构建中的一些工作以提高性能)。这对于开发很有用,因为 esbuild 可以在你工作时在后台为你重新构建应用程序。
有三种不同的增量构建 API
- 观察模式 告诉 esbuild 观察文件系统,并在你编辑和保存可能使构建失效的文件时自动为你重新构建。以下是一个示例
esbuild app.ts --bundle --outdir=dist --watch [watch] build finished, watching for changes...
let ctx = await esbuild.context({
entryPoints: ['app.ts'],
bundle: true,
outdir: 'dist',
})
await ctx.watch()
ctx, err := api.Context(api.BuildOptions{
EntryPoints: []string{"app.ts"},
Bundle: true,
Outdir: "dist",
})
err2 := ctx.Watch(api.WatchOptions{})
- 服务模式 启动一个本地开发服务器,该服务器提供最新构建的结果。传入的请求会自动启动新的构建,因此当你在浏览器中重新加载页面时,你的 Web 应用程序始终保持最新。以下是一个示例
esbuild app.ts --bundle --outdir=dist --serve > Local: http://127.0.0.1:8000/ > Network: http://192.168.0.1:8000/ 127.0.0.1:61302 - "GET /" 200 [1ms]
let ctx = await esbuild.context({
entryPoints: ['app.ts'],
bundle: true,
outdir: 'dist',
})
let { host, port } = await ctx.serve()
ctx, err := api.Context(api.BuildOptions{
EntryPoints: []string{"app.ts"},
Bundle: true,
Outdir: "dist",
})
server, err2 := ctx.Serve(api.ServeOptions{})
- 重新构建模式 允许你手动调用构建。这在将 esbuild 与其他工具集成时很有用(例如,使用自定义文件观察器或开发服务器而不是 esbuild 的内置观察器或开发服务器)。以下是一个示例
# The CLI does not have an API for "rebuild"
let ctx = await esbuild.context({
entryPoints: ['app.ts'],
bundle: true,
outdir: 'dist',
})
for (let i = 0; i < 5; i++) {
let result = await ctx.rebuild()
}
ctx, err := api.Context(api.BuildOptions{
EntryPoints: []string{"app.ts"},
Bundle: true,
Outdir: "dist",
})
for i := 0; i < 5; i++ {
result := ctx.Rebuild()
}
这三种增量构建 API 可以组合使用。要启用 实时重载(在你编辑和保存文件时自动重新加载页面),你需要在同一个上下文中同时启用 观察 和 服务。
当你完成上下文对象后,可以调用上下文上的 dispose()
来等待现有构建完成,停止观察和/或服务模式,并释放资源。
构建和上下文 API 都接受以下选项
#转换
这是 build 的一个有限的特殊情况,它转换表示内存中文件的代码字符串,在一个与任何其他文件完全断开的隔离环境中。常见用途包括压缩代码和将 TypeScript 转换为 JavaScript。以下是一个示例
echo 'let x: number = 1' | esbuild --loader=ts
let x = 1;
import * as esbuild from 'esbuild'
let ts = 'let x: number = 1'
let result = await esbuild.transform(ts, {
loader: 'ts',
})
console.log(result)
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)
}
}
使用字符串而不是文件作为输入对于某些用例来说更符合人体工程学。文件系统隔离具有一定的优势(例如,在浏览器中工作,不受附近 package.json
文件的影响)和一些劣势(例如,不能与 打包 或 插件 一起使用)。如果你的用例不适合转换 API,那么你应该使用更通用的 build API。
转换 API 接受以下选项
#JS 特定细节
esbuild 的 JS API 有异步和同步两种形式。建议使用 异步 API,因为它在所有环境中都能正常工作,并且更快、更强大。 同步 API 仅在 node 中工作,并且只能执行某些操作,但在某些特定于 node 的情况下有时是必要的。详细来说
#异步 API
异步 API 调用使用 promise 返回其结果。请注意,你可能需要在 node 中使用 .mjs
文件扩展名,因为使用了 import
和顶层 await
关键字
import * as esbuild from 'esbuild'
let result1 = await esbuild.transform(code, options)
let result2 = await esbuild.build(options)
优点
- 你可以将 插件 与异步 API 一起使用
- 当前线程不会被阻塞,因此你可以在此期间执行其他工作
- 你可以同时运行多个 esbuild API 调用,这些调用随后会分布到所有可用的 CPU 上,以实现最大性能
缺点
- 使用 promise 会导致代码更混乱,尤其是在 CommonJS 中,因为 顶层 await 不可用
- 在必须同步的情况下不起作用,例如在
require
中.extensions
#同步 API
同步 API 调用会内联返回其结果
let esbuild = require('esbuild')
let result1 = esbuild.transformSync(code, options)
let result2 = esbuild.buildSync(options)
优点
- 避免使用 promise 会导致代码更简洁,尤其是在 顶层 await 不可用时
- 在必须同步的情况下工作,例如在
require
中.extensions
缺点
- 你不能将 插件 与同步 API 一起使用,因为插件是异步的
- 它会阻塞当前线程,因此你无法在此期间执行其他工作
- 使用同步 API 会阻止 esbuild 并行化 esbuild API 调用
#在浏览器中
esbuild API 也可以在浏览器中使用 WebAssembly 在 Web Worker 中运行。要利用此功能,你需要安装 esbuild-wasm
包,而不是 esbuild
包
npm install esbuild-wasm
浏览器的 API 与 node 的 API 类似,除了你需要先调用 initialize()
,并且你需要传递 WebAssembly 二进制文件的 URL。同步版本的 API 也不可用。假设你正在使用打包器,这将类似于以下内容
import * as esbuild from 'esbuild-wasm'
await esbuild.initialize({
wasmURL: './node_modules/esbuild-wasm/esbuild.wasm',
})
let result1 = await esbuild.transform(code, options)
let result2 = esbuild.build(options)
如果你已经从 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>
如果你想将此 API 与 ECMAScript 模块一起使用,你应该导入 esm/browser.min.js
文件
<script type="module">
import * as esbuild from './node_modules/esbuild-wasm/esm/browser.min.js'
await esbuild.initialize({
wasmURL: './node_modules/esbuild-wasm/esbuild.wasm',
})
...
</script>
#通用选项
#打包
支持:构建
打包文件意味着将任何导入的依赖项内联到文件本身。此过程是递归的,因此依赖项的依赖项(等等)也将被内联。默认情况下,esbuild 不会 打包输入文件。打包必须像这样显式启用
esbuild in.js --bundle
import * as esbuild from 'esbuild'
console.log(await esbuild.build({
entryPoints: ['in.js'],
bundle: true,
outfile: 'out.js',
}))
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"in.js"},
Bundle: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
有关使用真实代码打包的示例,请参阅 入门指南。
请注意,打包与文件连接不同。将多个输入文件传递给 esbuild 并启用打包将创建多个独立的包,而不是将输入文件连接在一起。要使用 esbuild 连接一组文件,请将它们全部导入到单个入口点文件中,然后仅使用 esbuild 打包该文件。
#不可分析的导入
导入路径当前仅在它们是字符串文字或 glob 模式 时才会被打包。其他形式的导入路径不会被打包,而是会按原样保留在生成的输出中。这是因为打包是一个编译时操作,esbuild 不支持所有形式的运行时路径解析。以下是一些示例
// Analyzable imports (will be bundled by esbuild)
import 'pkg';
import('pkg');
require('pkg');
import(`./locale-${foo}.json`);
require(`./locale-${foo}.json`);
// Non-analyzable imports (will not be bundled by esbuild)
import(`pkg/${foo}`);
require(`pkg/${foo}`);
['pkg'].map(require);
解决不可分析导入的方法是将包含此问题代码的包标记为 外部,以便它不会包含在包中。然后,你需要确保在运行时,你的打包代码可以访问外部包的副本。
一些打包器,例如 Webpack,试图通过将所有可能访问的文件包含在包中,并在运行时模拟文件系统来支持所有形式的运行时路径解析。但是,运行时文件系统模拟超出了 esbuild 的范围,不会在 esbuild 中实现。如果您确实需要打包执行此操作的代码,您可能需要使用其他打包器而不是 esbuild。
#Glob 风格的导入
现在可以在某些有限的情况下打包在运行时评估的导入路径。导入路径表达式必须是字符串连接的形式,并且必须以 ./
或 ../
开头。字符串连接链中的每个非字符串表达式都成为 glob 模式中的通配符。一些例子
// These two forms are equivalent
const json1 = require('./data/' + kind + '.json')
const json2 = require(`./data/${kind}.json`)
当您执行此操作时,esbuild 将搜索文件系统以查找与模式匹配的所有文件,并将它们全部包含在包中,以及一个将匹配的导入路径映射到打包模块的映射。导入表达式将被替换为对该映射的查找。如果导入路径不在映射中,则会在运行时抛出错误。生成的代码将类似于以下内容(为了简洁起见,省略了不重要的部分)
// data/bar.json
var require_bar = ...;
// data/foo.json
var require_foo = ...;
// require("./data/**/*.json") in example.js
var globRequire_data_json = __glob({
"./data/bar.json": () => require_bar(),
"./data/foo.json": () => require_foo()
});
// example.js
var json1 = globRequire_data_json("./data/" + kind + ".json");
var json2 = globRequire_data_json(`./data/${kind}.json`);
此功能适用于 require(...)
和 import(...)
,因为它们都可以接受运行时表达式。它不适用于 import
和 export
语句,因为它们不能接受运行时表达式。如果您想阻止 esbuild 尝试打包这些导入,您应该将字符串连接表达式移出 require(...)
或 import(...)
。例如
// This will be bundled
const json1 = require('./data/' + kind + '.json')
// This will not be bundled
const path = './data/' + kind + '.json'
const json2 = require(path)
请注意,使用此功能意味着 esbuild 可能需要执行大量文件系统 I/O 来查找可能与模式匹配的所有文件。这是设计使然,不是错误。如果这是一个问题,有两种方法可以减少 esbuild 执行的文件系统 I/O 量
最简单的方法是将您想为给定运行时导入表达式导入的所有文件放在一个子目录中,然后将该子目录包含在模式中。这将 esbuild 限制在该子目录内搜索,因为 esbuild 在模式匹配期间不考虑
..
路径元素。另一种方法是阻止 esbuild 搜索任何子目录。esbuild 使用的模式匹配算法只允许通配符匹配包含
/
路径分隔符的内容,如果该通配符在模式中之前有/
。因此,例如'./data/' +
将匹配任何子目录中的x + '.json' x
,而'./data-' +
将只匹配顶层目录中的x + '.json' x
(但不在任何子目录中)。
#取消
支持:构建
如果您使用 rebuild 手动调用增量构建,您可能希望使用此取消 API 提早结束当前构建,以便您可以开始新的构建。您可以像这样操作
# The CLI does not have an API for "cancel"
import * as esbuild from 'esbuild'
import process from 'node:process'
let ctx = await esbuild.context({
entryPoints: ['app.ts'],
bundle: true,
outdir: 'www',
logLevel: 'info',
})
// Whenever we get some data over stdin
process.stdin.on('data', async () => {
try {
// Cancel the already-running build
await ctx.cancel()
// Then start a new build
console.log('build:', await ctx.rebuild())
} catch (err) {
console.error(err)
}
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
ctx, err := api.Context(api.BuildOptions{
EntryPoints: []string{"app.ts"},
Bundle: true,
Outdir: "www",
LogLevel: api.LogLevelInfo,
})
if err != nil {
os.Exit(1)
}
// Whenever we get some data over stdin
buf := make([]byte, 100)
for {
if n, err := os.Stdin.Read(buf); err != nil || n == 0 {
break
}
go func() {
// Cancel the already-running build
ctx.Cancel()
// Then start a new build
result := ctx.Rebuild()
fmt.Fprintf(os.Stderr, "build: %v\n", result)
}()
}
}
确保在开始新的构建之前等待取消操作完成(即,在使用 JavaScript 时 await
返回的 promise),否则下一个 rebuild 将为您提供刚刚取消的构建,该构建尚未结束。请注意,插件 on-end 回调 将在构建是否被取消的情况下仍然运行。
#实时重载
支持:构建
实时重载是一种开发方法,您可以在其中同时打开并查看浏览器和代码编辑器。当您编辑并保存源代码时,浏览器会自动重新加载,重新加载的应用程序版本包含您的更改。这意味着您可以更快地迭代,因为您不必在每次更改后手动切换到浏览器、重新加载,然后切换回代码编辑器。例如,在更改 CSS 时非常有用。
esbuild 没有直接用于实时重载的 API。相反,您可以通过结合 监视模式(在您编辑并保存文件时自动启动构建)和 服务模式(服务最新构建,但阻塞直到完成)以及一小段仅在开发期间添加到应用程序中的客户端 JavaScript 代码来构建实时重载。
esbuild app.ts --bundle --outdir=www --watch --servedir=www
import * as esbuild from 'esbuild'
let ctx = await esbuild.context({
entryPoints: ['app.ts'],
bundle: true,
outdir: 'www',
})
await ctx.watch()
let { host, port } = await ctx.serve({
servedir: 'www',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
ctx, err := api.Context(api.BuildOptions{
EntryPoints: []string{"app.ts"},
Bundle: true,
Outdir: "www",
})
if err != nil {
os.Exit(1)
}
err2 := ctx.Watch(api.WatchOptions{})
if err2 != nil {
os.Exit(1)
}
result, err3 := ctx.Serve(api.ServeOptions{
Servedir: "www",
})
if err3 != nil {
os.Exit(1)
}
}
第二步是在您的 JavaScript 中添加一些代码,这些代码订阅 /esbuild
服务器发送事件 源。当您收到 change
事件时,您可以重新加载页面以获取应用程序的最新版本。您可以在一行代码中执行此操作
new EventSource('/esbuild').addEventListener('change', () => location.reload())
就是这样!如果您在浏览器中加载应用程序,现在当您编辑并保存文件时,页面应该会自动重新加载(假设没有构建错误)。
这应该只在开发期间包含,并且不应包含在生产中。在生产中删除此代码的一种方法是使用 if 语句(例如 if (!window.IS_PRODUCTION)
)对其进行保护,然后使用 define 在生产中将 window.IS_PRODUCTION
设置为 true
。
#实时重载注意事项
以这种方式实现实时重载有一些已知的注意事项
这些事件仅在 esbuild 的输出发生变化时触发。它们不会在与正在监视的构建无关的文件发生更改时触发。如果您的 HTML 文件引用了 esbuild 不了解的其他文件,并且这些文件发生了更改,您可以手动重新加载页面,或者您可以实现自己的实时重载基础设施,而不是使用 esbuild 的内置行为。
EventSource
API 应该会自动为您重新连接。但是,Firefox 中存在一个错误,如果服务器暂时无法访问,则会破坏此功能。解决方法是使用任何其他浏览器,如果发生这种情况,手动重新加载页面,或者编写更复杂的代码,如果出现连接错误,则手动关闭并重新创建EventSource
对象。浏览器供应商已决定不实现不带 TLS 的 HTTP/2。这意味着在使用
http://
协议时,每个/esbuild
事件源将占用您宝贵的 6 个每域 HTTP/1.1 连接中的一个。因此,如果您打开了超过六个使用此实时重载技术的 HTTP 选项卡,您将无法在其中一些选项卡中使用实时重载(并且其他事情也可能会中断)。解决方法是 启用https://
协议。
#CSS 的热重载
change
事件还包含其他信息以启用更高级的使用案例。它目前包含 added
、removed
和 updated
数组,其中包含自上次构建以来已更改的文件的路径,这些路径可以用以下 TypeScript 接口描述
interface ChangeEvent {
added: string[]
removed: string[]
updated: string[]
}
下面的代码示例启用了 CSS 的“热重载”,即在不重新加载页面的情况下自动更新 CSS。如果到达的事件与 CSS 无关,则整个页面将作为回退重新加载
new EventSource('/esbuild').addEventListener('change', e => {
const { added, removed, updated } = JSON.parse(e.data)
if (!added.length && !removed.length && updated.length === 1) {
for (const link of document.getElementsByTagName("link")) {
const url = new URL(link.href)
if (url.host === location.host && url.pathname === updated[0]) {
const next = link.cloneNode()
next.href = updated[0] + '?' + Math.random().toString(36).slice(2)
next.onload = () => link.remove()
link.parentNode.insertBefore(next, link.nextSibling)
return
}
}
}
location.reload()
})
#JavaScript 的热重载
esbuild 目前没有实现 JavaScript 的热重载。可以透明地实现 CSS 的热重载,因为 CSS 是无状态的,但 JavaScript 是有状态的,因此您无法像 CSS 那样透明地实现 JavaScript 的热重载。
一些其他开发服务器仍然为 JavaScript 实现了热重载,但这需要额外的 API,有时需要特定于框架的黑客,有时会在编辑会话期间引入瞬态状态相关的错误。这样做超出了 esbuild 的范围。如果您需要 JavaScript 的热重载,欢迎您使用其他工具而不是 esbuild。
但是,使用 esbuild 的实时重载,您可以将应用程序的当前 JavaScript 状态持久化到 sessionStorage
中,以便在页面重新加载后更容易恢复应用程序的 JavaScript 状态。如果您的应用程序加载速度很快(对于用户的利益,它应该已经很快),使用 JavaScript 的实时重载几乎可以与使用 JavaScript 的热重载一样快。
#平台
默认情况下,esbuild 的打包器配置为生成针对浏览器的代码。如果您的打包代码旨在在 node 中运行,您应该将平台设置为 node
esbuild app.js --bundle --platform=node
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
当平台设置为 browser
(默认值)时
当启用 打包 时,默认输出 格式 设置为
iife
,它将生成的 JavaScript 代码包装在一个立即调用的函数表达式中,以防止变量泄漏到全局范围。如果一个包在其
package.json
文件中为browser
字段指定了一个映射,esbuild 将使用该映射将特定文件或模块替换为其浏览器友好的版本。例如,一个包可能包含path
与path-browserify
的替换。主字段 设置设置为
browser,
,但有一些额外的特殊行为:如果一个包提供了module, main module
和main
入口点,但没有提供browser
入口点,那么如果该包曾经使用require()
导入,则使用main
而不是module
。此行为提高了与通过将函数分配给module.exports
来导出函数的 CommonJS 模块的兼容性。如果您想禁用此额外的特殊行为,您可以显式地将 主字段 设置设置为browser,
。module, main 条件 设置自动包含
browser
条件。这会改变package.json
文件中exports
字段的解释方式,以优先考虑特定于浏览器的代码。如果没有配置自定义 条件,则还会包含特定于 Webpack 的
module
条件。module
条件由包作者使用,以提供一个可树状摇动的 ESM 替代方案,而不是 CommonJS 文件,而不会创建 双包危害。您可以通过显式配置一些自定义条件(即使是空列表)来阻止包含module
条件。在使用 build API 时,如果所有 最小化 选项都已启用,则所有
process.
表达式将自动 定义 为env. NODE_ENV "production"
,否则为"development"
。这只有在process
、process.env
和process.env.NODE_ENV
尚未定义的情况下才会发生。此替换对于避免基于 React 的代码立即崩溃是必要的(因为process
是一个 node API,而不是一个 web API)。字符序列
</script>
将在 JavaScript 代码中转义,字符序列</style>
将在 CSS 代码中转义。这样做是为了防止您将 esbuild 的输出直接内联到 HTML 文件中。这可以通过 esbuild 的 supported 功能禁用,方法是将inline-script
(对于 JavaScript)和/或inline-style
(对于 CSS)设置为false
。
当平台设置为 node
时
当启用 打包 时,默认输出 格式 设置为
cjs
,它代表 CommonJS(node 使用的模块格式)。使用export
语句的 ES6 风格导出将被转换为 CommonJSexports
对象上的 getter。所有 内置 node 模块(例如
fs
)都会自动标记为 外部,因此当打包器尝试打包它们时不会导致错误。主字段 设置设置为
main,
。这意味着对于提供module module
和main
的包,树状摇动可能不会发生,因为树状摇动适用于 ECMAScript 模块,但不适用于 CommonJS 模块。不幸的是,一些包错误地将
module
视为“浏览器代码”而不是“ECMAScript 模块代码”,因此这种默认行为是兼容性所必需的。如果您想启用树状摇动并知道这样做是安全的,您可以手动将 主字段 设置配置为module,
。main 当 conditions 设置自动包含
node
条件时,它会改变package.json
文件中exports
字段的解释方式,优先使用特定于 Node.js 的代码。如果没有配置自定义 条件,则还会包含特定于 Webpack 的
module
条件。module
条件由包作者使用,以提供一个可树状摇动的 ESM 替代方案,而不是 CommonJS 文件,而不会创建 双包危害。您可以通过显式配置一些自定义条件(即使是空列表)来阻止包含module
条件。当 format 设置为
cjs
但入口点为 ESM 时,esbuild 会为任何命名导出添加特殊注释,以便使用 ESM 语法从生成的 CommonJS 文件中导入这些命名导出。Node.js 的文档提供了更多关于 Node.js 如何检测 CommonJS 命名导出 的信息。binary
加载器将使用 Node.js 内置的Buffer.from
API 将捆绑包中嵌入的 Base64 数据解码为Uint8Array
。这比 esbuild 可以做到的其他方法更快,因为它是在 Node.js 中使用原生代码实现的。
当平台设置为 neutral
时
当启用 捆绑 时,默认的输出 格式 设置为
esm
,它使用 ECMAScript 2015(即 ES6)中引入的export
语法。如果此默认设置不合适,您可以更改输出格式。默认情况下,main 字段 设置为空。如果您想使用 npm 风格的包,您可能需要将其配置为其他值,例如
main
,用于 Node.js 使用的标准 main 字段。conditions 设置不会自动包含任何特定于平台的值。
另请参阅 为浏览器捆绑 和 为 Node.js 捆绑。
#重新构建
支持:构建
如果您需要重复使用相同的选项调用 esbuild 的 build API,您可能需要使用此 API。例如,如果您正在实现自己的文件监视器服务,这将很有用。重新构建比再次构建更有效,因为来自先前构建的一些数据被缓存,如果原始文件自上次构建以来没有更改,则可以重复使用这些数据。目前,重建 API 使用两种形式的缓存
如果文件元数据自上次构建以来没有更改,则文件将存储在内存中,并且不会从文件系统中重新读取。此优化仅适用于文件系统路径。它不适用于由 插件 创建的虚拟模块。
解析后的 AST 存储在内存中,如果文件内容自上次构建以来没有更改,则会避免重新解析 AST。此优化适用于由插件创建的虚拟模块,以及文件系统模块,只要虚拟模块路径保持不变。
以下是如何进行重新构建
# The CLI does not have an API for "rebuild"
import * as esbuild from 'esbuild'
let ctx = await esbuild.context({
entryPoints: ['app.js'],
bundle: true,
outfile: 'out.js',
})
// Call "rebuild" as many times as you want
for (let i = 0; i < 5; i++) {
let result = await ctx.rebuild()
}
// Call "dispose" when you're done to free up resources
ctx.dispose()
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
ctx, err := api.Context(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Outfile: "out.js",
})
if err != nil {
os.Exit(1)
}
// Call "Rebuild" as many times as you want
for i := 0; i < 5; i++ {
result := ctx.Rebuild()
if len(result.Errors) > 0 {
os.Exit(1)
}
}
// Call "Dispose" when you're done to free up resources
ctx.Dispose()
}
#服务
支持:构建
服务模式启动一个 Web 服务器,该服务器将您的代码提供给您设备上的浏览器。以下是一个将 src/app.ts
捆绑到 www/js/app.js
,然后还通过 https://127.0.0.1:8000/
提供 www
目录的示例
esbuild src/app.ts --outdir=www/js --bundle --servedir=www
import * as esbuild from 'esbuild'
let ctx = await esbuild.context({
entryPoints: ['src/app.ts'],
outdir: 'www/js',
bundle: true,
})
let { host, port } = await ctx.serve({
servedir: 'www',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
ctx, err := api.Context(api.BuildOptions{
EntryPoints: []string{"src/app.ts"},
Outdir: "www/js",
Bundle: true,
})
if err != nil {
os.Exit(1)
}
server, err2 := ctx.Serve(api.ServeOptions{
Servedir: "www",
})
if err2 != nil {
os.Exit(1)
}
// Returning from main() exits immediately in Go.
// Block forever so we keep serving and don't exit.
<-make(chan struct{})
}
如果您创建了包含以下内容的文件 www/index.html
,那么当您导航到 https://127.0.0.1:8000/
时,src/app.ts
中包含的代码将加载。
<script src="js/app.js"></script>
使用 esbuild 内置的 Web 服务器而不是其他 Web 服务器的一个好处是,无论何时您重新加载,esbuild 提供的文件始终是最新的。其他开发设置不一定如此。一种常见的设置是运行一个本地文件监视器,该监视器在输入文件发生更改时重建输出文件,然后单独运行一个本地文件服务器来提供这些输出文件。但这意味着在编辑后重新加载可能会重新加载旧的输出文件,如果重建尚未完成。使用 esbuild 的 Web 服务器,每个传入请求都会启动一个重建(如果还没有进行重建),然后等待当前重建完成,然后再提供文件。这意味着 esbuild 永远不会提供过时的构建结果。
请注意,此 Web 服务器仅用于开发。请勿在生产环境中使用它。
#参数
serve API 的参数如下
# Enable serve mode
--serve
# Set the port
--serve=9000
# Set the host and port (IPv4)
--serve=127.0.0.1:9000
# Set the host and port (IPv6)
--serve=[::1]:9000
# Set the directory to serve
--servedir=www
# Enable HTTPS
--keyfile=your.key --certfile=your.cert
# Specify a fallback HTML file
--serve-fallback=some-file.html
interface ServeOptions {
port?: number
host?: string
servedir?: string
keyfile?: string
certfile?: string
fallback?: 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
Keyfile string
Certfile string
Fallback string
OnRequest func(ServeOnRequestArgs)
}
type ServeOnRequestArgs struct {
RemoteAddress string
Method string
Path string
Status int
TimeInMS int
}
host
默认情况下,esbuild 使 Web 服务器在所有 IPv4 网络接口上可用。这对应于主机地址
0.0.0.0
。如果您想配置不同的主机(例如,仅在127.0.0.1
环回接口上提供服务,而不向网络公开任何内容),您可以使用此参数指定主机。如果您需要使用 IPv6 而不是 IPv4,您只需要指定一个 IPv6 主机地址。在 IPv6 中,
127.0.0.1
环回接口的等效项是::1
,0.0.0.0
通用接口的等效项是::
。port
HTTP 端口可以在这里可选地配置。如果省略,它将默认为一个开放端口,优先考虑 8000 到 8009 范围内的端口。
servedir
这是一个额外的内容目录,用于 esbuild 的 HTTP 服务器,当传入请求与任何生成的输出文件路径不匹配时,它将提供这些内容,而不是提供 404 错误。这使您可以将 esbuild 用作通用的本地 Web 服务器。
例如,您可能希望创建一个
index.html
文件,然后将servedir
设置为"."
以提供当前目录(包括index.html
文件)。如果您没有设置servedir
,那么 esbuild 将只提供构建结果,而不会提供任何其他文件。keyfile
和certfile
如果您使用
keyfile
和certfile
将私钥和证书传递给 esbuild,那么 esbuild 的 Web 服务器将使用https://
协议而不是http://
协议。有关更多信息,请参阅 启用 HTTPS。fallback
这是一个 HTML 文件,用于 esbuild 的 HTTP 服务器,当传入请求与任何生成的输出文件路径不匹配时,它将提供这些内容,而不是提供 404 错误。您可以将其用于自定义的“未找到”页面。您也可以将其用作 单页应用程序 的入口点,该应用程序会修改当前 URL,因此需要从许多不同的 URL 同时提供服务。
onRequest
对于每个传入请求,都会调用一次,并提供有关请求的一些信息。CLI 使用此回调为每个请求打印一条日志消息。time 字段是生成请求数据的耗时,但不包括将请求流式传输到客户端的耗时。
请注意,这在请求完成之后调用。无法使用此回调以任何方式修改请求。如果您想这样做,您应该 在 esbuild 前面放置一个代理。
#返回值
# The CLI will print the host and port like this:
> Local: http://127.0.0.1:8000/
interface ServeResult {
host: string
port: number
}
type ServeResult struct {
Host string
Port uint16
}
host
这是 Web 服务器最终使用的主机。它将是
0.0.0.0
(即在所有可用网络接口上提供服务),除非配置了自定义主机。如果您正在使用 CLI 并且主机是0.0.0.0
,则所有可用网络接口将作为主机打印出来。port
这是 Web 服务器最终使用的端口。如果您没有指定端口,您将需要使用它,因为 esbuild 最终会选择一个任意的开放端口,您需要知道它选择了哪个端口才能连接到它。
#启用 HTTPS
默认情况下,esbuild 的 Web 服务器使用 http://
协议。但是,某些现代 Web 功能对 HTTP 网站不可用。如果您想使用这些功能,那么您需要告诉 esbuild 使用 https://
协议。
要使用 esbuild 启用 HTTPS
生成一个自签名证书。有很多方法可以做到这一点。以下是一种方法,假设您已安装
openssl
命令openssl req -x509 -newkey rsa:4096 -keyout your.key -out your.cert -days 9999 -nodes -subj /CN=127.0.0.1
使用
keyfile
和certfile
serve 参数 将your.key
和your.cert
传递给 esbuild。当您加载页面时,单击浏览器中的可怕警告(自签名证书不安全,但这并不重要,因为我们只是在进行本地开发)。
如果您有比这更复杂的需求,您仍然可以 在 esbuild 前面放置一个代理,并使用它来代替 HTTPS。请注意,如果您在加载页面时看到消息 Client
,那么您正在使用错误的协议。将浏览器 URL 栏中的 http://
替换为 https://
。
请记住,esbuild 的 HTTPS 支持与安全性无关。在 esbuild 中启用 HTTPS 的唯一原因是,浏览器已经使在没有经过这些额外步骤的情况下,使用某些现代 Web 功能进行本地开发变得不可能。请勿将 esbuild 的开发服务器用于任何需要安全性的内容。它仅用于本地开发,并且没有考虑生产环境。
#自定义服务器行为
无法挂钩到 esbuild 的本地服务器以自定义服务器本身的行为。相反,应通过在 esbuild 前面放置一个代理来自定义行为。
以下是一个简单的代理服务器示例,可帮助您入门,使用 Node.js 内置的 http
模块。它添加了一个自定义的 404 页面,而不是 esbuild 的默认 404 页面
import * as esbuild from 'esbuild'
import http from 'node:http'
// Start esbuild's server on a random local port
let ctx = await esbuild.context({
// ... your build options go here ...
})
// The return value tells us where esbuild's local server is
let { host, port } = await ctx.serve({ servedir: '.' })
// 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 上启动一个代理服务器。在开发过程中,您将在浏览器中加载 https://127.0.0.1:3000,它会与代理通信。此示例演示了在 esbuild 处理请求后修改响应,但您也可以在 esbuild 处理请求之前修改或替换请求。
您可以使用这样的代理做很多事情,包括
- 注入您自己的 404 页面(上面的示例)
- 自定义路由到文件系统上文件的映射
- 将某些路由重定向到 API 服务器,而不是重定向到 esbuild
如果您有更高级的需求,您也可以使用真正的代理,例如 nginx。
#Tsconfig
支持:构建
通常,build API 会自动发现 tsconfig.json
文件并在构建期间读取其内容。但是,您也可以配置一个自定义的 tsconfig.json
文件来代替。如果您需要使用不同的设置对同一代码进行多次构建,这将很有用
esbuild app.ts --bundle --tsconfig=custom-tsconfig.json
import * as esbuild from 'esbuild'
await esbuild.build({
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
此选项可用于将您的 tsconfig.json
文件传递给 transform API,该 API 不会访问文件系统。它也可以用于将您的 tsconfig.json
文件的内容内联传递给 build API,而无需将其写入文件。使用它看起来像这样
echo 'class Foo { foo }' | esbuild --loader=ts --tsconfig-raw='{"compilerOptions":{"useDefineForClassFields":false}}'
import * as esbuild from 'esbuild'
let ts = 'class Foo { foo }'
let result = await esbuild.transform(ts, {
loader: 'ts',
tsconfigRaw: `{
"compilerOptions": {
"useDefineForClassFields": false,
},
}`,
})
console.log(result.code)
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": false,
},
}`,
})
if len(result.Errors) == 0 {
fmt.Printf("%s", result.Code)
}
}
#监视
支持:构建
启用监视模式会告诉 esbuild 监听文件系统的更改,并在发生可能使构建无效的文件更改时自动重建。使用它看起来像这样
esbuild app.js --outfile=out.js --bundle --watch [watch] build finished, watching for changes...
import * as esbuild from 'esbuild'
let ctx = await esbuild.context({
entryPoints: ['app.js'],
outfile: 'out.js',
bundle: true,
})
await ctx.watch()
console.log('watching...')
package main
import "fmt"
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
ctx, err := api.Context(api.BuildOptions{
EntryPoints: []string{"app.js"},
Outfile: "out.js",
Bundle: true,
Write: true,
})
if err != nil {
os.Exit(1)
}
err2 := ctx.Watch(api.WatchOptions{})
if err2 != nil {
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 struct{})
}
如果您想在将来的某个时间停止监视模式,您可以调用上下文对象的 dispose
来终止文件监视器
# Use Ctrl+C to stop the CLI in watch mode
import * as esbuild from 'esbuild'
let ctx = await esbuild.context({
entryPoints: ['app.js'],
outfile: 'out.js',
bundle: true,
})
await ctx.watch()
console.log('watching...')
await new Promise(r => setTimeout(r, 10 * 1000))
await ctx.dispose()
console.log('stopped watching')
package main
import "fmt"
import "github.com/evanw/esbuild/pkg/api"
import "os"
import "time"
func main() {
ctx, err := api.Context(api.BuildOptions{
EntryPoints: []string{"app.js"},
Outfile: "out.js",
Bundle: true,
Write: true,
})
if err != nil {
os.Exit(1)
}
err2 := ctx.Watch(api.WatchOptions{})
if err2 != nil {
os.Exit(1)
}
fmt.Printf("watching...\n")
time.Sleep(10 * time.Second)
ctx.Dispose()
fmt.Printf("stopped watching\n")
}
esbuild 中的监视模式是使用轮询而不是特定于操作系统的文件系统 API 来实现的,以确保可移植性。轮询系统旨在使用相对较少的 CPU,与更传统的轮询系统相比,后者会一次扫描整个目录树。文件系统仍然会定期扫描,但每次扫描只检查文件的随机子集,这意味着对文件的更改将在更改后不久被发现,但不一定立即被发现。
使用当前的启发式方法,大型项目应该大约每 2 秒完全扫描一次,因此在最坏的情况下,可能需要长达 2 秒才能发现更改。但是,在发现更改后,更改的路径将进入最近更改的路径的简短列表,这些路径将在每次扫描时进行检查,因此对最近更改的文件的进一步更改应该几乎立即被发现。
请注意,如果您不想使用基于轮询的方法,仍然可以使用 esbuild 的 rebuild API 和您选择的任何文件监视库来自己实现监视模式。
如果您使用的是 CLI,请记住,当 esbuild 的 stdin 关闭时,监视模式将终止。这可以防止 esbuild 意外地比父进程存活更长时间,并意外地继续消耗系统资源。如果您有需要 esbuild 即使在父进程完成之后也永远监视的用例,可以使用 --watch=
而不是 --watch
。
#输入
#入口点
支持:构建
这是一个文件数组,每个文件都作为捆绑算法的输入。它们被称为“入口点”,因为每个入口点都应该作为最初被评估的脚本,然后加载它所代表的所有其他代码方面。与其在您的页面中使用 <script>
标签加载许多库,不如使用 import
语句将它们导入到您的入口点(或导入到另一个随后被导入到您的入口点的文件中)。
简单的应用程序只需要一个入口点,但是如果存在多个逻辑上独立的代码组(例如主线程和工作线程),或者应用程序具有独立的相对无关的区域(例如登录页面、编辑器页面和设置页面),则额外的入口点可能会有用。单独的入口点有助于引入关注点分离,并有助于减少浏览器需要下载的不必要代码量。如果适用,启用 代码拆分 可以进一步减少浏览到第二个页面的下载大小,该页面的入口点与已访问的第一个页面共享一些已下载的代码。
指定入口点的简单方法是只传递一个文件路径数组
esbuild home.ts settings.ts --bundle --outdir=out
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
这将生成两个输出文件,out/home.js
和 out/settings.js
,分别对应于两个入口点 home.ts
和 settings.ts
。
为了进一步控制如何从相应的输入入口点派生输出文件的路径,您应该查看这些选项
此外,您还可以使用另一种入口点语法为每个单独的入口点指定一个完全自定义的输出路径
esbuild out1=home.ts out2=settings.ts --bundle --outdir=out
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: [
{ out: 'out1', in: 'home.ts'},
{ out: 'out2', in: '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{
EntryPointsAdvanced: []api.EntryPoint{{
OutputPath: "out1",
InputPath: "home.ts",
}, {
OutputPath: "out2",
InputPath: "settings.ts",
}},
Bundle: true,
Write: true,
Outdir: "out",
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
这将生成两个输出文件,out/out1.js
和 out/out2.js
,分别对应于两个入口点 home.ts
和 settings.ts
。
#加载器
此选项更改如何解释给定的输入文件。例如,js
加载器将文件解释为 JavaScript,而 css
加载器将文件解释为 CSS。有关所有内置加载器的完整列表,请参见 内容类型 页面。
为给定文件类型配置加载器可以让您使用 import
语句或 require
调用加载该文件类型。例如,将 .png
文件扩展名配置为使用 数据 URL 加载器意味着导入 .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
import * as esbuild from 'esbuild'
await esbuild.build({
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 配置加载器如下所示
echo 'import pkg = require("./pkg")' | esbuild --loader=ts --bundle
import * as esbuild from 'esbuild'
await esbuild.build({
stdin: {
contents: 'import pkg = require("./pkg")',
loader: 'ts',
resolveDir: '.',
},
bundle: true,
outfile: 'out.js',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
Stdin: &api.StdinOptions{
Contents: "import pkg = require('./pkg')",
Loader: api.LoaderTS,
ResolveDir: ".",
},
Bundle: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
transform API 调用只接受一个加载器,因为它不涉及与文件系统交互,因此不处理文件扩展名。为 transform API 配置加载器(在本例中为 ts
加载器)如下所示
echo 'let x: number = 1' | esbuild --loader=ts
let x = 1;
import * as esbuild from 'esbuild'
let ts = 'let x: number = 1'
let result = await esbuild.transform(ts, {
loader: 'ts',
})
console.log(result.code)
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)
}
}
#标准输入
支持:构建
通常,build API 调用将一个或多个文件名作为输入。但是,此选项可用于在文件系统上根本不存在模块的情况下运行构建。它被称为“stdin”,因为它对应于将文件管道到命令行上的 stdin。
除了指定 stdin 文件的内容之外,您还可以选择指定解析目录(用于确定相对导入的位置)、源文件(用于错误消息和源映射的文件名)以及 加载器(用于确定如何解释文件内容)。CLI 没有指定解析目录的方法。相反,它会自动设置为当前工作目录。
以下是使用此功能的方法
echo 'export * from "./another-file"' | esbuild --bundle --sourcefile=imaginary-file.js --loader=ts --format=cjs
import * as esbuild from 'esbuild'
let result = await esbuild.build({
stdin: {
contents: `export * from "./another-file"`,
// These are all optional:
resolveDir: './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)
}
}
#输出内容
#横幅
使用此选项在生成的 JavaScript 和 CSS 文件的开头插入任意字符串。这通常用于插入注释
esbuild app.js --banner:js=//comment --banner:css=/*comment*/
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
这类似于 页脚,它在结尾处插入而不是开头。
请注意,如果您将非注释代码插入 CSS 文件,请注意 CSS 会忽略所有在非 @import
规则(除了 @charset
规则)之后的 @import
规则,因此使用横幅注入 CSS 规则可能会意外地禁用外部样式表的导入。
#字符集
默认情况下,esbuild 的输出是 ASCII 唯一的。任何非 ASCII 字符都使用反斜杠转义序列进行转义。一个原因是浏览器默认情况下会错误地解释非 ASCII 字符,这会导致混淆。您必须在您的 HTML 中显式添加 <meta
或使用正确的 Content-
标头提供它,以便浏览器不会篡改您的代码。另一个原因是非 ASCII 字符会显著 减慢浏览器的解析器速度。但是,使用转义序列会使生成的输出稍微变大,并且也会使它更难阅读。
如果您希望 esbuild 在不使用转义序列的情况下打印原始字符,并且您已确保浏览器将您的代码解释为 UTF-8,则可以通过设置字符集来禁用字符转义
echo 'let π = Math.PI' | esbuild
let \u03C0 = Math.PI;
echo 'let π = Math.PI' | esbuild --charset=utf8
let π = Math.PI;
import * as esbuild from 'esbuild'
let js = 'let π = Math.PI'
(await esbuild.transform(js)).code
'let \\u03C0 = Math.PI;\n'
(await esbuild.transform(js, {
charset: 'utf8',
})).code
'let π = Math.PI;\n'
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。
#页脚
使用此选项在生成的 JavaScript 和 CSS 文件的末尾插入任意字符串。这通常用于插入注释
esbuild app.js --footer:js=//comment --footer:css=/*comment*/
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
这类似于 横幅,它在开头处插入而不是结尾。
#格式
这设置了生成的 JavaScript 文件的输出格式。目前可以配置三个可能的值:iife
、cjs
和 esm
。当没有指定输出格式时,esbuild 会在启用 捆绑 时为您选择一个输出格式(如下所述),或者在禁用 捆绑 时不进行任何格式转换。
#IIFE
iife
格式代表“立即调用函数表达式”,旨在在浏览器中运行。将您的代码包装在函数表达式中可确保您的代码中的任何变量不会意外地与全局范围内的变量冲突。如果您的入口点具有您想在浏览器中作为全局变量公开的导出,则可以使用 全局名称 设置配置该全局变量的名称。当未指定输出格式、启用 捆绑 以及 平台 设置为 browser
(默认情况下为 browser
)时,将自动启用 iife
格式。指定 iife
格式如下所示
echo 'alert("test")' | esbuild --format=iife
(() => {
alert("test");
})();
import * as esbuild from 'esbuild'
let js = 'alert("test")'
let result = await esbuild.transform(js, {
format: 'iife',
})
console.log(result.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。当未指定输出格式、启用 捆绑 以及 平台 设置为 node
时,将自动启用 cjs
格式。指定 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";
import * as esbuild from 'esbuild'
let js = 'export default "test"'
let result = await esbuild.transform(js, {
format: 'cjs',
})
console.log(result.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 模块”。它假设环境支持 import
和 export
语法。具有 CommonJS 模块语法导出的入口点将被转换为 module.exports
值的单个 default
导出。当未指定输出格式、启用 捆绑 以及 平台 设置为 neutral
时,将自动启用 esm
格式。指定 esm
格式如下所示
echo 'module.exports = "test"' | esbuild --format=esm
...
var require_stdin = __commonJS({
"<stdin>"(exports, module) {
module.exports = "test";
}
});
export default require_stdin();
import * as esbuild from 'esbuild'
let js = 'module.exports = "test"'
let result = await esbuild.transform(js, {
format: 'esm',
})
console.log(result.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 中使用,但您必须显式地将其加载为模块。如果您从另一个模块导入它,这将自动发生。否则
- 在浏览器中,您可以使用
<script
加载模块。不要忘记src=" file.js" type=" module"> </script> type="
,因为这会以微妙且令人困惑的方式破坏您的代码(省略module" type="
意味着所有顶层变量最终将进入全局范围,这将与其他 JavaScript 文件中具有相同名称的顶层变量发生冲突)。module"
- 在 node 中,您可以使用
node
加载模块。请注意,node 需要file.mjs .mjs
扩展名,除非您已在package.json
文件中配置了"type":
。您可以使用 esbuild 中的 out 扩展名 设置来自定义 esbuild 生成的文件的输出扩展名。您可以阅读有关在 node 中使用 ECMAScript 模块的更多信息 此处。"module"
#全局名称
此选项仅在 格式 设置为 iife
(代表立即调用函数表达式)时才重要。它设置用于存储入口点导出的全局变量的名称
echo 'module.exports = "test"' | esbuild --format=iife --global-name=xyz
import * as esbuild from 'esbuild'
let js = 'module.exports = "test"'
let result = await esbuild.transform(js, {
format: 'iife',
globalName: 'xyz',
})
console.log(result.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.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"]'
import * as esbuild from 'esbuild'
let js = 'module.exports = "test"'
let result = await esbuild.transform(js, {
format: 'iife',
globalName: 'example.versions["1.0"]',
})
console.log(result.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.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();
})();
#法律注释
“法律注释”被认为是 JS 中的任何语句级注释或 CSS 中的任何规则级注释,其中包含 @license
或 @preserve
,或者以 //!
或 /*!
开头。这些注释默认情况下会保留在输出文件中,因为这遵循了代码原始作者的意图。但是,可以使用以下选项之一配置此行为
none
不保留任何法律注释。inline
保留所有法律注释。eof
将所有法律注释移动到文件末尾。linked
将所有法律注释移动到.LEGAL.txt
文件中,并使用注释链接到它们。external
将所有法律注释移动到.LEGAL.txt
文件中,但不要链接到它们。
当启用 捆绑 时,默认行为为 eof
,否则为 inline
。设置法律注释模式如下所示
esbuild app.js --legal-comments=eof
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
请注意,对于 JS 而言,“语句级” 和对于 CSS 而言的“规则级” 意味着注释必须出现在允许多个语句或规则的上下文中,例如在顶层作用域或语句或规则块中。因此,表达式内部或声明级别的注释不被视为合法注释。
#行限制
此设置是防止 esbuild 生成具有非常长行的输出文件的一种方法,这有助于在实现不佳的文本编辑器中提高编辑性能。将其设置为正整数以告诉 esbuild 在超过该字节数后不久结束给定行。例如,这会在行超过约 80 个字符后不久将其换行。
esbuild app.ts --line-limit=80
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['app.ts'],
lineLimit: 80,
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.ts"},
LineLimit: 80,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
行在超过限制后被截断,而不是在之前被截断,因为在超过限制时进行检查比预测何时将超过限制更简单,并且因为在生成输出文件时避免回退和重写内容更快。因此,限制只是近似的。
此设置适用于 JavaScript 和 CSS,即使在禁用缩小的情况下也能正常工作。请注意,启用此设置会使您的文件更大,因为额外的换行符在文件中占用更多空间(即使在 gzip 压缩后)。
#拆分
支持:构建
这将启用“代码拆分”,它有两个目的。
在多个入口点之间共享的代码将被拆分到一个单独的共享文件中,这两个入口点都将导入该文件。这样,如果用户首先浏览到一个页面,然后浏览到另一个页面,他们不必从头开始下载第二个页面的所有 JavaScript,如果共享部分已经下载并被他们的浏览器缓存。
通过异步
import()
表达式引用的代码将被拆分到一个单独的文件中,并且只有在该表达式被求值时才会加载。这允许您通过仅在启动时下载所需的代码来提高应用程序的初始下载时间,然后在以后需要时延迟下载其他代码。如果没有启用代码拆分,
import()
表达式将变为Promise
。这仍然保留了表达式的异步语义,但意味着导入的代码包含在同一个包中,而不是被拆分到一个单独的文件中。.resolve() .then(() => require())
启用代码拆分后,您还必须使用 outdir 设置配置输出目录。
esbuild home.ts about.ts --bundle --splitting --outdir=out --format=esm
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
#输出位置
#允许覆盖
支持:构建
启用此设置允许输出文件覆盖输入文件。它默认情况下未启用,因为这样做意味着覆盖您的源代码,如果您的代码未签入,这会导致数据丢失。但是,支持这一点使某些工作流程更容易,因为无需使用临时目录。因此,当您想要故意覆盖您的源代码时,可以启用它。
esbuild app.js --outdir=. --allow-overwrite
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
#资产名称
支持:构建
此选项控制当 加载器 设置为 file
时生成的附加输出文件的名称。它使用带有占位符的模板配置输出路径,这些占位符将在生成输出路径时用特定于文件的值替换。例如,指定 assets/
的资产名称模板会将所有资产放入输出目录内的名为 assets
的子目录中,并在文件名中包含资产的内容哈希。这样看起来像这样
esbuild app.js --asset-names=assets/[name]-[hash] --loader:.png=file --bundle --outdir=out
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['app.js'],
assetNames: 'assets/[name]-[hash]',
loader: { '.png': 'file' },
bundle: true,
outdir: 'out',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
AssetNames: "assets/[name]-[hash]",
Loader: map[string]api.Loader{
".png": api.LoaderFile,
},
Bundle: true,
Outdir: "out",
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
在资产路径模板中可以使用四个占位符。
[dir]
这是从包含资产文件的目录到 outbase 目录的相对路径。它的目的是通过镜像输出目录内的输入目录结构来帮助资产输出路径看起来更美观。
[name]
这是资产的原始文件名,不包括扩展名。例如,如果资产最初名为
image.png
,那么[name]
将在模板中被替换为image
。使用此占位符不是必需的;它只存在是为了提供人性化的资产名称,以便更容易调试。[hash]
这是资产的内容哈希,这对于避免名称冲突很有用。例如,您的代码可能会导入
components/
和button/ icon.png components/
,在这种情况下,您需要使用哈希来区分这两个都名为select/ icon.png icon
的资产。[ext]
这是资产的文件扩展名(即最后一个
.
字符之后的任何内容)。它可以用来将不同类型的资产放入不同的目录。例如,--asset-names=
可能会将名为assets/ [ext]/ [name]-[hash] image.png
的资产写入assets/
。png/ image-CQFGD2NG.png
资产路径模板不需要包含文件扩展名。资产的原始文件扩展名将在模板替换后自动添加到输出路径的末尾。
#块名称
支持:构建
此选项控制当启用 代码拆分 时自动生成的共享代码块的文件名。它使用带有占位符的模板配置输出路径,这些占位符将在生成输出路径时用特定于块的值替换。例如,指定 chunks/
的块名称模板会将所有生成的块放入输出目录内的名为 chunks
的子目录中,并在文件名中包含块的内容哈希。这样看起来像这样
esbuild app.js --chunk-names=chunks/[name]-[hash] --bundle --outdir=out --splitting --format=esm
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
在块路径模板中可以使用三个占位符。
[name]
这目前将始终是文本
chunk
,尽管此占位符在将来的版本中可能会采用其他值。[hash]
这是块的内容哈希。包含此内容对于在生成多个共享代码块的情况下区分不同的块是必要的。
[ext]
这是块的文件扩展名(即最后一个
.
字符之后的任何内容)。它可以用来将不同类型的块放入不同的目录。例如,--chunk-names=
可能会将一个块写入chunks/ [ext]/ [name]-[hash] chunks/
。css/ chunk-DEFJT7KY.css
块路径模板不需要包含文件扩展名。将自动将相应内容类型的配置的 输出扩展名 添加到模板替换后的输出路径的末尾。
请注意,此选项仅控制自动生成的共享代码块的名称。它不控制与入口点相关的输出文件的名称。这些的名称目前由原始入口点文件相对于 outbase 目录的路径决定,并且此行为无法更改。将来会添加一个额外的 API 选项,让您更改入口点输出文件的名称。
#入口点名称
支持:构建
此选项控制与每个输入入口点文件对应的输出文件的文件名。它使用带有占位符的模板配置输出路径,这些占位符将在生成输出路径时用特定于文件的值替换。例如,指定 [dir]/
的入口点名称模板会在文件名中包含输出文件的内容哈希,并将文件放入输出目录,可能在子目录下(请参阅下面有关 [dir]
的详细信息)。这样看起来像这样
esbuild src/main-app/app.js --entry-names=[dir]/[name]-[hash] --outbase=src --bundle --outdir=out
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['src/main-app/app.js'],
entryNames: '[dir]/[name]-[hash]',
outbase: 'src',
bundle: true,
outdir: 'out',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"src/main-app/app.js"},
EntryNames: "[dir]/[name]-[hash]",
Outbase: "src",
Bundle: true,
Outdir: "out",
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
在入口点路径模板中可以使用四个占位符。
[dir]
这是从包含输入入口点文件的目录到 outbase 目录的相对路径。它的目的是帮助您避免不同子目录中同名入口点之间的冲突。
例如,如果存在两个入口点
src/
和pages/ home/ index.ts src/
,outbase 目录是pages/ about/ index.ts src
,入口点名称模板是[dir]/[name]
,则输出目录将包含pages/
和home/ index.js pages/
。如果入口点名称模板只是about/ index.js [name]
,则打包将失败,因为输出目录中将存在两个具有相同输出路径index.js
的输出文件。[name]
这是入口点的原始文件名,不包括扩展名。例如,如果输入入口点文件名为
app.js
,则[name]
将在模板中被替换为app
。[hash]
这是输出文件的内容哈希,它可以用来充分利用浏览器缓存。在入口点名称中添加
[hash]
意味着 esbuild 将计算一个与相应输出文件中的所有内容(以及在启用 代码拆分 时它导入的任何输出文件)相关的哈希。该哈希旨在仅当与该输出文件相关的任何输入文件发生更改时才更改。之后,您可以让您的 Web 服务器告诉浏览器永远缓存这些文件(实际上,您可以说它们在很长时间后过期,例如一年)。然后,您可以使用 元文件 中的信息来确定哪个输出文件路径对应于哪个输入入口点,以便您知道在您的
<script>
标签中包含哪个路径。[ext]
这是入口点文件将被写入的扩展名(即 输出扩展名 设置,而不是原始文件扩展名)。它可以用来将不同类型的入口点放入不同的目录。例如,
--entry-names=
可能会将entries/ [ext]/ [name] app.ts
的输出文件写入entries/
。js/ app.js
入口点路径模板不需要包含文件扩展名。将根据文件类型自动将适当的 输出扩展名 添加到模板替换后的输出路径的末尾。
#输出扩展名
支持:构建
此选项允许您将 esbuild 生成的文件的文件扩展名自定义为除 .js
或 .css
之外的其他内容。特别是,.mjs
和 .cjs
文件扩展名在节点中具有特殊含义(它们分别表示 ESM 和 CommonJS 格式的文件)。如果您使用 esbuild 生成多个文件,并且必须使用 outdir 选项而不是 outfile 选项,则此选项很有用。您可以像这样使用它
esbuild app.js --bundle --outdir=dist --out-extension:.js=.mjs
import * as esbuild from 'esbuild'
await esbuild.build({
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",
OutExtension: map[string]string{
".js": ".mjs",
},
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
#Outbase
支持:构建
如果您的构建包含多个位于不同目录中的入口点,则目录结构将相对于 outbase 目录复制到 输出目录 中。例如,如果存在两个入口点 src/
和 src/
,并且 outbase 目录是 src
,则输出目录将包含 pages/
和 pages/
。以下是使用方法
esbuild src/pages/home/index.ts src/pages/about/index.ts --bundle --outdir=out --outbase=src
import * as esbuild from 'esbuild'
await esbuild.build({
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/
。
#Outdir
支持:构建
此选项设置构建操作的输出目录。例如,此命令将生成一个名为 out
的目录。
esbuild app.js --bundle --outdir=out
import * as esbuild from 'esbuild'
await esbuild.build({
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 之前手动清空输出目录。
如果你的构建包含多个位于不同目录的入口点,目录结构将从所有输入入口点路径的最近公共祖先目录开始复制到输出目录。例如,如果存在两个入口点 src/
和 src/
,输出目录将包含 home/
和 about/
。如果你想自定义此行为,你应该更改outbase 目录。
#输出文件
支持:构建
此选项设置构建操作的输出文件名。这仅适用于只有一个入口点的情况。如果有多个入口点,你必须使用outdir 选项来指定输出目录。使用 outfile 的方式如下
esbuild app.js --bundle --outfile=out.js
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
#公共路径
支持:构建
这在与外部文件 加载器结合使用时很有用。默认情况下,该加载器使用 default
导出将导入文件的名称作为字符串导出。公共路径选项允许你在此加载器加载的每个文件的导出字符串前面添加一个基本路径
esbuild app.js --bundle --loader:.png=file --public-path=https://www.example.com/v1 --outdir=out
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
#写入
支持:构建
构建 API 调用可以将文件直接写入文件系统,也可以将本应写入的文件作为内存缓冲区返回。默认情况下,CLI 和 JavaScript API 写入文件系统,而 Go API 则不写入。要使用内存缓冲区
import * as esbuild from 'esbuild'
let result = await esbuild.build({
entryPoints: ['app.js'],
sourcemap: 'external',
write: false,
outdir: 'out',
})
for (let out of result.outputFiles) {
console.log(out.path, out.contents, out.hash, out.text)
}
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 %s\n", out.Path, out.Contents, out.Hash)
}
}
hash
属性是 contents
字段的哈希值,为了方便起见提供。哈希算法(目前为XXH64)依赖于实现,并且可能在 esbuild 版本之间随时更改。
#路径解析
#别名
支持:构建
此功能允许你在捆绑时用一个包替换另一个包。下面的示例用包 newpkg
替换包 oldpkg
esbuild app.js --bundle --alias:oldpkg=newpkg
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
write: true,
alias: {
'oldpkg': 'newpkg',
},
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Write: true,
Alias: map[string]string{
"oldpkg": "newpkg",
},
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
这些新的替换在 esbuild 的所有其他路径解析逻辑之前发生。此功能的一个用例是在你无法控制的第三方代码中用浏览器友好的包替换仅限节点的包。
请注意,当使用别名替换导入路径时,生成的导入路径将在工作目录中解析,而不是在包含具有导入路径的源文件的目录中解析。如果需要,可以使用工作目录 功能设置 esbuild 使用的工作目录。
#条件
支持:构建
此功能控制如何解释 package.json
中的 exports
字段。可以使用 conditions 设置添加自定义条件。你可以指定任意数量的这些条件,它们的含义完全由包作者决定。Node 目前只认可 development
和 production
自定义条件用于推荐使用。以下是如何添加自定义条件 custom1
和 custom2
的示例
esbuild src/app.js --bundle --conditions=custom1,custom2
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
#条件的工作原理
条件允许你在不同情况下将相同的导入路径重定向到不同的文件位置。包含条件和路径的重定向映射存储在包的 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"
}
}
}
条件按它们在 JSON 文件中出现的顺序进行检查。因此上面的示例的行为类似于
if (importPath === './foo') {
if (conditions.has('import')) return './imported.mjs'
if (conditions.has('require')) return './required.cjs'
return './fallback.js'
}
默认情况下,有五个具有特殊行为的条件内置于 esbuild,并且无法禁用
default
此条件始终处于活动状态。它旨在最后出现,并在没有其他条件适用时提供备用方案。当你以原生方式在 node 中运行代码时,此条件也处于活动状态。
import
此条件仅在导入路径来自 ESM
import
语句或import()
表达式时处于活动状态。它可以用来提供特定于 ESM 的代码。当你以原生方式在 node 中运行代码时,此条件也处于活动状态(但仅在 ESM 上下文中)。require
此条件仅在导入路径来自 CommonJS
require()
调用时处于活动状态。它可以用来提供特定于 CommonJS 的代码。当你以原生方式在 node 中运行代码时,此条件也处于活动状态(但仅在 CommonJS 上下文中)。browser
此条件仅在 esbuild 的platform 设置设置为
browser
时处于活动状态。它可以用来提供特定于浏览器的代码。当你以原生方式在 node 中运行代码时,此条件不处于活动状态。node
此条件仅在 esbuild 的platform 设置设置为
node
时处于活动状态。它可以用来提供特定于节点的代码。当你以原生方式在 node 中运行代码时,此条件也处于活动状态。
当platform 设置为 browser
或 node
且未配置任何自定义条件时,以下条件也会自动包含在内。如果配置了任何自定义条件(即使是空列表),则此条件将不再自动包含在内
module
此条件可以用来告诉 esbuild 为给定的导入路径选择 ESM 变体,以便在捆绑时提供更好的树摇动。当你以原生方式在 node 中运行代码时,此条件不处于活动状态。它特定于捆绑器,起源于 Webpack。
请注意,当你使用 require
和 import
条件时,你的包可能会在捆绑包中出现多次! 这是一个微妙的问题,可能会导致错误,因为你的代码状态的重复副本会使生成的捆绑包膨胀。这通常被称为双包危害。
避免双包危害的一种方法是将所有代码放在 require
条件中作为 CommonJS,并让 import
条件只是一个轻量级的 ESM 包装器,它在你的包上调用 require
并使用 ESM 语法重新导出包。但是,这种方法不会提供良好的树摇动,因为 esbuild 不会对 CommonJS 模块进行树摇动。
避免双包危害的另一种方法是使用特定于捆绑器的 module
条件来指示捆绑器始终加载你的包的 ESM 版本,同时让 node 始终回退到你的包的 CommonJS 版本。import
和 module
都旨在与 ESM 一起使用,但与 import
不同,module
条件始终处于活动状态,即使导入路径是使用 require
调用加载的。这在捆绑器中效果很好,因为捆绑器支持使用 require
加载 ESM,但这不是 node 可以做的事情,因为 node 故意没有实现使用 require
加载 ESM。
#外部
支持:构建
你可以将文件或包标记为外部,以将其从构建中排除。导入将被保留(对于 iife
和 cjs
格式使用 require
,对于 esm
格式使用 import
),而不是被捆绑,并且将在运行时进行评估。
这有几个用途。首先,它可以用来从你的捆绑包中修剪掉你已知永远不会执行的代码路径的无用代码。例如,一个包可能包含仅在 node 中运行的代码,但你只会在浏览器中使用该包。它也可以用来在 node 中从无法捆绑的包中导入代码。例如,fsevents
包包含一个原生扩展,esbuild 不支持。将某些东西标记为外部的方式如下
echo 'require("fsevents")' > app.js
esbuild app.js --bundle --external:fsevents --platform=node
// app.js
require("fsevents");
import * as esbuild from 'esbuild'
import fs from 'node:fs'
fs.writeFileSync('app.js', 'require("fsevents")')
await esbuild.build({
entryPoints: ['app.js'],
outfile: 'out.js',
bundle: true,
platform: 'node',
external: ['fsevents'],
})
package main
import "io/ioutil"
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
ioutil.WriteFile("app.js", []byte("require(\"fsevents\")"), 0644)
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Outfile: "out.js",
Bundle: true,
Write: true,
Platform: api.PlatformNode,
External: []string{"fsevents"},
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
你也可以在外部路径中使用 *
通配符来将与该模式匹配的所有文件标记为外部。例如,你可以使用 *.png
来删除所有 .png
文件,或者使用 /images/*
来删除所有以 /images/
开头的路径
esbuild app.js --bundle "--external:*.png" "--external:/images/*"
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['app.js'],
outfile: 'out.js',
bundle: true,
external: ['*.png', '/images/*'],
})
package main
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,
External: []string{"*.png", "/images/*"},
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
外部路径在路径解析之前和之后都应用,这使你能够匹配源代码中的导入路径和绝对文件系统路径。如果外部路径在任一情况下都匹配,则该路径被认为是外部路径。具体行为如下
在路径解析开始之前,导入路径将与所有外部路径进行检查。此外,如果外部路径看起来像包路径(即不以
/
或./
或../
开头),导入路径将被检查以查看它们是否具有该包路径作为路径前缀。这意味着
--external:
隐式地也意味着@foo/ bar --external:
,它与导入路径@foo/ bar/* @foo/
匹配。因此,它也将bar/ baz @foo/bar
包中的所有路径标记为外部路径。在路径解析结束之后,解析后的绝对路径将与所有不看起来像包路径的外部路径(即那些以
/
或./
或../
开头的路径)进行检查。但在检查之前,外部路径将与当前工作目录连接,然后进行规范化,成为绝对路径(即使它包含*
通配符)。这意味着你可以使用
--external:
将./dir/* dir
目录中的所有内容标记为外部。请注意,前导./
很重要。使用--external:
而不是dir/* --external:
将被视为包路径,并且不会在路径解析结束之后进行检查。./dir/*
#主字段
支持:构建
当你导入 node 中的包时,该包的 package.json
文件中的 main
字段决定导入哪个文件(以及许多其他规则)。包括 esbuild 在内的主要 JavaScript 捆绑器允许你指定在解析包时尝试的额外 package.json
字段。至少有三个这样的字段在使用中
main
这是所有旨在与 node 一起使用的包的标准字段。名称
main
在 node 的模块解析逻辑本身中被硬编码。因为它旨在与 node 一起使用,所以可以合理地预期此字段中的文件路径是一个 CommonJS 风格的模块。module
此字段来自一项提案,该提案描述了如何将 ECMAScript 模块集成到 node 中。因此,可以合理地预期此字段中的文件路径是一个 ECMAScript 风格的模块。该提案没有被 node 采用(node 使用
"type":
代替),但它被主要捆绑器采用,因为 ECMAScript 风格的模块可以带来更好的树摇动,即死代码移除。"module" 对于包作者:一些包错误地将
module
字段用于特定于浏览器的代码,将特定于节点的代码留给main
字段。这可能是因为 node 忽略了module
字段,而人们通常只将捆绑器用于特定于浏览器的代码。但是,捆绑特定于节点的代码也很有价值(例如,它减少了下载和启动时间),将特定于浏览器的代码放在module
中的包会阻止捆绑器有效地进行树摇动。如果你试图在包中发布特定于浏览器的代码,请使用browser
字段代替。browser
此字段来自一项提案,该提案允许捆绑器用浏览器友好的版本替换特定于节点的文件或模块。它允许你指定一个备用的特定于浏览器的入口点。请注意,一个包可以同时使用
browser
和module
字段(参见下面的说明)。
默认主字段取决于当前的platform 设置。这些默认值应该与现有的包生态系统最广泛地兼容。但如果你想自定义它们,可以这样做
esbuild app.js --bundle --main-fields=module,main
import * as esbuild from 'esbuild'
await esbuild.build({
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 和浏览器-vs-节点兼容性矩阵中。为此,您需要使用browser
字段的扩展形式,它是一个映射,而不仅仅是一个字符串。
{
"main": "./node-cjs.js",
"module": "./node-esm.js",
"browser": {
"./node-cjs.js": "./browser-cjs.js",
"./node-esm.js": "./browser-esm.js"
}
}
main
字段预期为 CommonJS,而module
字段预期为 ESM。关于使用哪种模块格式的决定与是否使用浏览器特定或节点特定变体的决定无关。如果您省略了这四个条目中的一个,那么您就有可能选择错误的变体。例如,如果您省略了 CommonJS 浏览器构建的条目,那么可能会选择 CommonJS 节点构建。
请注意,使用main
、module
和browser
是旧的实现方式。还有一种更新的实现方式,您可能更喜欢使用它:package.json
中的exports
字段。它提供了一组不同的权衡。例如,它可以让您更精确地控制包中所有子路径的导入(而main
字段只让您控制入口点),但它可能会导致您的包根据您的配置方式被多次导入。
#节点路径
支持:构建
节点的模块解析算法支持一个名为NODE_PATH
的环境变量,其中包含一个用于解析导入路径的全局目录列表。除了所有父目录中的node_modules
目录外,还会在这些路径中搜索包。您可以使用 CLI 中的环境变量以及 JS 和 Go API 中的数组将此目录列表传递给 esbuild。
NODE_PATH=someDir esbuild app.js --bundle --outfile=out.js
import * as esbuild from 'esbuild'
await esbuild.build({
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
传递多个目录,则必须在 Unix 上使用:
,在 Windows 上使用;
分隔它们。这与 Node 本身使用的格式相同。
#包
支持:构建
使用此设置将您包的所有依赖项从捆绑包中排除。当为节点捆绑时,这很有用,因为许多 npm 包使用 esbuild 在捆绑时不支持的节点特定功能(例如__dirname
、import.meta.url
、fs.readFileSync
和*.node
本机二进制模块)。使用它看起来像这样
esbuild app.js --bundle --packages=external
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
packages: 'external',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Packages: api.PackagesExternal,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
启用此功能会自动将所有看起来像 npm 包的导入路径(即,不以.
或..
路径组件开头且不是绝对路径的路径)标记为外部。它与手动将每个依赖项传递给external具有相同的效果,但更简洁。如果您想自定义哪些依赖项是外部的,哪些不是,那么您应该使用external而不是此设置。
请注意,此设置仅在启用捆绑时才有效。还要注意,将导入路径标记为外部是在导入路径被任何配置的别名重写之后发生的,因此别名功能在使用此设置时仍然有效。
#保留符号链接
支持:构建
此设置反映了节点中的--preserve-symlinks
设置。如果您使用该设置(或 Webpack 中类似的resolve.symlinks
设置),您可能需要在 esbuild 中也启用此设置。它可以像这样启用
esbuild app.js --bundle --preserve-symlinks --outfile=out.js
import * as esbuild from 'esbuild'
await esbuild.build({
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 通过原始文件路径(即不跟随符号链接的路径)而不是实际文件路径(即跟随符号链接后的路径)来确定文件标识。这对于某些目录结构可能是有益的。请记住,这意味着如果有多个符号链接指向一个文件,则该文件可能会被赋予多个标识,这会导致它在生成的输出文件中多次出现。
注意:术语“符号链接”是指符号链接,指的是文件系统功能,其中路径可以重定向到另一个路径。
#解析扩展名
支持:构建
节点使用的解析算法支持隐式文件扩展名。您可以require(
,它将按顺序检查./file
、./file.js
、./file.json
和./file.node
。包括 esbuild 在内的现代捆绑器将此概念扩展到其他文件类型。esbuild 中隐式文件扩展名的完整顺序可以使用解析扩展名设置进行自定义,该设置默认为.tsx,
esbuild app.js --bundle --resolve-extensions=.ts,.js
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
请注意,esbuild 故意不将新的.mjs
和.cjs
扩展名包含在此列表中。节点的解析算法不将这些视为隐式文件扩展名,因此 esbuild 也不将它们视为隐式文件扩展名。如果您想导入具有这些扩展名的文件,您应该在导入路径中显式添加扩展名,或者更改此设置以包含您希望隐式的其他扩展名。
#工作目录
支持:构建
此 API 选项允许您指定用于构建的工作目录。它通常默认为您用于调用 esbuild API 的进程的当前工作目录。esbuild 使用工作目录来执行一些不同的操作,包括将作为 API 选项给出的相对路径解析为绝对路径,以及在日志消息中将绝对路径漂亮地打印为相对路径。以下是自定义 esbuild 工作目录的方法
cd "/var/tmp/custom/working/directory"
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['file.js'],
absWorkingDir: '/var/tmp/custom/working/directory',
outfile: 'out.js',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"file.js"},
AbsWorkingDir: "/var/tmp/custom/working/directory",
Outfile: "out.js",
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
注意:如果您使用的是Yarn Plug'n'Play,请记住,此工作目录用于搜索 Yarn 的清单文件。如果您从无关的目录运行 esbuild,则必须将此工作目录设置为包含清单文件的目录(或其子目录之一),以便 esbuild 能够找到清单文件。
#转换
#JSX
此选项告诉 esbuild 如何处理 JSX 语法。以下是可用的选项
transform
这告诉 esbuild 使用许多使用 JSX 语法的库之间共享的通用转换将 JSX 转换为 JS。每个 JSX 元素都将转换为对JSX 工厂函数的调用,该函数以元素的组件(或对于片段而言,以JSX 片段)作为第一个参数。第二个参数是一个道具数组(如果没有道具,则为
null
)。任何存在的子元素都将成为第二个参数之后的附加参数。如果您想在每个文件的基础上配置此设置,您可以使用
// @jsxRuntime
注释来实现。这是Babel 的 JSX 插件遵循的约定,esbuild 也遵循此约定。classic preserve
这会保留输出中的 JSX 语法,而不是将其转换为函数调用。JSX 元素被视为一等语法,并且仍然受到其他设置(如缩小和属性混淆)的影响。
请注意,这意味着输出文件不再是有效的 JavaScript 代码。此功能旨在用于在捆绑后,当您希望通过另一个工具转换 esbuild 输出文件中的 JSX 语法时使用。
automatic
此转换是在React 17+ 中引入的,并且非常特定于 React。它会自动从JSX 导入源生成
import
语句,并引入许多关于如何处理语法的特殊情况。细节过于复杂,无法在此处描述。有关更多信息,请阅读React 关于其新的 JSX 转换的文档。如果您想启用此转换的开发模式版本,则需要另外启用JSX dev设置。如果您想在每个文件的基础上配置此设置,您可以使用
// @jsxRuntime
注释来实现。这是Babel 的 JSX 插件遵循的约定,esbuild 也遵循此约定。automatic
以下是如何将 JSX 转换设置为preserve
的示例
echo '<div/>' | esbuild --jsx=preserve --loader=jsx
<div />;
import * as esbuild from 'esbuild'
let result = await esbuild.transform('<div/>', {
jsx: 'preserve',
loader: 'jsx',
})
console.log(result.code)
package main
import "fmt"
import "github.com/evanw/esbuild/pkg/api"
func main() {
result := api.Transform("<div/>", api.TransformOptions{
JSX: api.JSXPreserve,
Loader: api.LoaderJSX,
})
if len(result.Errors) == 0 {
fmt.Printf("%s", result.Code)
}
}
#JSX dev
如果JSX转换已设置为automatic
,则启用此设置会导致 esbuild 自动将文件名和源位置注入到每个 JSX 元素中。您的 JSX 库然后可以使用此信息来帮助调试。如果 JSX 转换已设置为除automatic
以外的其他内容,则此设置将不起作用。以下是如何启用此设置的示例
echo '<a/>' | esbuild --loader=jsx --jsx=automatic
import { jsx } from "react/jsx-runtime";
/* @__PURE__ */ jsx("a", {});
echo '<a/>' | esbuild --loader=jsx --jsx=automatic --jsx-dev
import { jsxDEV } from "react/jsx-dev-runtime";
/* @__PURE__ */ jsxDEV("a", {}, void 0, false, {
fileName: "<stdin>",
lineNumber: 1,
columnNumber: 1
}, this);
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['app.jsx'],
jsxDev: true,
jsx: 'automatic',
outfile: 'out.js',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.jsx"},
JSXDev: true,
JSX: api.JSXAutomatic,
Outfile: "out.js",
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
#JSX 工厂
这将设置对每个 JSX 元素调用的函数。通常,像这样的 JSX 表达式
<div>Example text</div>
被编译成对React.createElement
的函数调用,如下所示
React.createElement("div", null, "Example text");
您可以通过更改 JSX 工厂来调用除React.createElement
以外的其他内容。例如,要调用函数h
(其他库(如Preact)使用)
echo '<div/>' | esbuild --jsx-factory=h --loader=jsx
/* @__PURE__ */ h("div", null);
import * as esbuild from 'esbuild'
let result = await esbuild.transform('<div/>', {
jsxFactory: 'h',
loader: 'jsx',
})
console.log(result.code)
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,您只需通过将以下内容添加到您的tsconfig.json
文件中来配置 TypeScript 的 JSX,esbuild 应该会自动将其拾取,而无需进行配置
{
"compilerOptions": {
"jsxFactory": "h"
}
}
如果您想在每个文件的基础上配置此设置,您可以使用// @jsx
注释来实现。请注意,当JSX转换已设置为automatic
时,此设置不适用。
#JSX 片段
这将设置对每个 JSX 片段调用的函数。通常,像这样的 JSX 片段表达式
<>Stuff</>
被编译成使用React.Fragment
组件,如下所示
React.createElement(React.Fragment, null, "Stuff");
您可以通过更改 JSX 片段来使用除React.Fragment
以外的组件。例如,要使用组件Fragment
(其他库(如Preact)使用)
echo '<>x</>' | esbuild --jsx-fragment=Fragment --loader=jsx
/* @__PURE__ */ React.createElement(Fragment, null, "x");
import * as esbuild from 'esbuild'
let result = await esbuild.transform('<>x</>', {
jsxFragment: 'Fragment',
loader: 'jsx',
})
console.log(result.code)
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"
}
}
如果您想在每个文件的基础上配置此设置,您可以使用// @jsxFrag
注释来实现。请注意,当JSX转换已设置为automatic
时,此设置不适用。
#JSX 导入源
如果JSX转换已设置为automatic
,则设置此选项可以让您更改 esbuild 用于自动从其 JSX 辅助函数导入的库。请注意,这仅适用于特定于 React 17+的 JSX 转换。如果您将 JSX 导入源设置为your-pkg
,则该包必须至少公开以下导出
import { createElement } from "your-pkg"
import { Fragment, jsx, jsxs } from "your-pkg/jsx-runtime"
import { Fragment, jsxDEV } from "your-pkg/jsx-dev-runtime"
/jsx-runtime
和/jsx-dev-runtime
子路径是硬编码的,不能更改。当JSX dev 模式关闭时,jsx
和jsxs
导入被使用,而当 JSX dev 模式打开时,jsxDEV
导入被使用。这些的含义在React 关于其新的 JSX 转换的文档中进行了描述。当元素具有道具扩展后跟key
道具时,无论 JSX dev 模式如何,createElement
导入都会被使用,它看起来像这样
return <div {...props} key={key} />
以下是如何将 JSX 导入源设置为preact
的示例
esbuild app.jsx --jsx-import-source=preact --jsx=automatic
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['app.jsx'],
jsxImportSource: 'preact',
jsx: 'automatic',
outfile: 'out.js',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.jsx"},
JSXImportSource: "preact",
JSX: api.JSXAutomatic,
Outfile: "out.js",
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
或者,如果您使用的是 TypeScript,您只需通过将以下内容添加到您的tsconfig.json
文件中来配置 TypeScript 的 JSX 导入源,esbuild 应该会自动将其拾取,而无需进行配置
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}
如果您想在每个文件的基础上控制此设置,您可以在每个文件中使用// @jsxImportSource
注释来实现。您可能还需要添加// @jsxRuntime
注释,如果JSX转换尚未通过其他方式设置,或者如果您希望在每个文件的基础上设置它。
#JSX 副作用
默认情况下,esbuild 假设 JSX 表达式没有副作用,这意味着它们被标注为 /* @__PURE__ */
注释,并在捆绑过程中被移除,因为它们没有被使用。这遵循了 JSX 用于虚拟 DOM 的常见用法,并适用于绝大多数 JSX 库。但是,有些人编写了没有此属性的 JSX 库(特别是 JSX 表达式可以具有任意副作用,并且在未使用时无法删除)。如果您正在使用这样的库,您可以使用此设置告诉 esbuild JSX 表达式具有副作用。
esbuild app.jsx --jsx-side-effects
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['app.jsx'],
outfile: 'out.js',
jsxSideEffects: true,
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.jsx"},
Outfile: "out.js",
JSXSideEffects: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
#支持
此设置允许您在单个语法功能级别自定义 esbuild 的不支持语法功能集。例如,您可以使用它告诉 esbuild BigInts 不受支持,以便 esbuild 在您尝试使用它时生成错误。通常,当您使用 target
设置时,会为您配置此设置,您通常应该使用此设置而不是此设置。如果除了此设置之外还指定了目标,则此设置将覆盖目标指定的任何内容。
以下是一些您可能希望使用此设置而不是或除了设置目标之外的示例。
JavaScript 运行时通常会对较新的语法功能进行快速实现,这比等效的旧 JavaScript 速度慢,您可以通过告诉 esbuild 假装此语法功能不受支持来获得加速。例如,V8 有一个 关于对象扩展的长期性能错误,可以通过手动复制属性而不是使用对象扩展语法来避免。
除了 esbuild 的
target
设置识别的那些之外,还有许多其他 JavaScript 实现,它们可能不支持某些功能。如果您要针对这样的实现,您可以使用此设置使用自定义语法功能兼容性集配置 esbuild,而无需更改 esbuild 本身。例如,TypeScript 的 JavaScript 解析器可能不支持 任意模块命名空间标识符名称,因此您可能希望在针对 TypeScript 的 JavaScript 解析器时将其关闭。您可能正在使用其他工具处理 esbuild 的输出,并且您可能希望 esbuild 转换某些功能,而其他工具转换其他某些功能。例如,如果您使用 esbuild 将文件单独转换为 ES5,但随后将输出提供给 Webpack 进行捆绑,您可能希望保留
import()
表达式,即使它们是 ES5 中的语法错误。
如果您希望 esbuild 将某个语法功能视为不受支持,您可以像这样指定它。
esbuild app.js --supported:bigint=false
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['app.js'],
supported: {
'bigint': false,
},
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Supported: map[string]bool{
"bigint": false,
},
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
语法功能使用 esbuild 特定的功能名称指定。完整的特征名称集如下所示。
JavaScript
arbitrary-module-namespace-names
array-spread
arrow
async-await
async-generator
bigint
class
class-field
class-private-accessor
class-private-brand-check
class-private-field
class-private-method
class-private-static-accessor
class-private-static-field
class-private-static-method
class-static-blocks
class-static-field
const-and-let
decorators
default-argument
destructuring
dynamic-import
exponent-operator
export-star-as
for-await
for-of
function-or-class-property-access
generator
hashbang
import-assertions
import-meta
inline-script
logical-assignment
nested-rest-binding
new-target
node-colon-prefix-import
node-colon-prefix-require
nullish-coalescing
object-accessors
object-extensions
object-rest-spread
optional-catch-binding
optional-chain
regexp-dot-all-flag
regexp-lookbehind-assertions
regexp-match-indices
regexp-named-capture-groups
regexp-set-notation
regexp-sticky-and-unicode-flags
regexp-unicode-property-escapes
rest-argument
template-literal
top-level-await
typeof-exotic-object-is-object
unicode-escapes
using
CSS
color-functions
gradient-double-position
gradient-interpolation
gradient-midpoints
hwb
hex-rgba
inline-style
inset-property
is-pseudo-class
modern-rgb-hsl
nesting
rebecca-purple
#目标
这将为生成的 JavaScript 和/或 CSS 代码设置目标环境。它告诉 esbuild 将对这些环境来说太新的 JavaScript 语法转换为将在这
请注意,这仅与语法功能有关,而不是 API。它不会自动添加 polyfills 用于这些环境不使用的
每个目标环境都是一个环境名称,后跟一个版本号。目前支持以下环境名称。
chrome
deno
edge
firefox
hermes
ie
ios
node
opera
rhino
safari
此外,您还可以指定 JavaScript 语言版本,例如 es2020
。默认目标是 esnext
,这意味着默认情况下,esbuild 将假
esbuild app.js --target=es2020,chrome58,edge16,firefox57,node12,safari11
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['app.js'],
target: [
'es2020',
'chrome58',
'edge16',
'firefox57',
'node12',
'safari11',
],
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.EngineEdge, Version: "16"},
{Name: api.EngineFirefox, Version: "57"},
{Name: api.EngineNode, Version: "12"},
{Name: api.EngineSafari, Version: "11"},
},
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
您可以参考 JavaScript 加载器 以了解有关哪些语法功能是在哪些语言版本中引入的详细信息。请记住,虽然 JavaScript 语言版本(例如 es2020
)是按年份标识的,但那是规范被批准的年份。它与所有主要浏览器实现该规范的年份无关,该年份通常早于或晚于该年份。
如果您使用 esbuild 尚未支持转换为当前语言目标的语法功能,esbuild 将在使用不支持的语法的地方生成错误。例如,当针对 es5
语言版本时,这种情况经常发生,因为 esbuild 仅支持将大多数较新的 JavaScript 语法功能转换为 es6
。
如果您需要除了或代替 target
提供的功能之外,在单个功能级别自定义支持的语法功能集,您可以使用 supported
设置。
#优化
#定义
此功能提供了一种方法,可以将全局标识符替换为常量表达式。它可以成为一种在构建之间更改某些代码的行为而无需更改代码本身的方法。
echo 'hooks = DEBUG && require("hooks")' | esbuild --define:DEBUG=true
hooks = require("hooks");
echo 'hooks = DEBUG && require("hooks")' | esbuild --define:DEBUG=false
hooks = false;
import * as esbuild from 'esbuild'let js = 'hooks = DEBUG && require("hooks")'(await esbuild.transform(js, {
define: { DEBUG: 'true' },
})).code
'hooks = require("hooks");\n'
(await esbuild.transform(js, {
define: { DEBUG: 'false' },
})).code
'hooks = false;\n'
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)
}
}
每个 define
条目将一个标识符映射到一个包含表达式的代码字符串。字符串中的表达式必须是 JSON 对象(null、布尔值、数字、字符串、数组或对象)或单个标识符。除了数组和对象之外的替换表达式将被内联替换,这意味着它们可以参与常量折叠。数组和对象替换表达式存储在变量中,然后使用标识符引用,而不是内联替换,这避免了替换值的重复副本,但意味着这些值不参与常量折叠。
如果您想用字符串文字替换某些内容,请记住,传递给 esbuild 的替换值本身必须包含引号,因为每个 define
条目都映射到包含代码的字符串。省略引号意味着替换值是一个标识符而不是字符串。这在下面的示例中得到了证明。
echo 'id, str' | esbuild --define:id=text --define:str=\"text\"
text, "text";
import * as esbuild from 'esbuild'(await esbuild.transform('id, str', {
define: { id: 'text', str: '"text"' },
})).code
'text, "text";\n'
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 脚本使用 CLI 时相关,人们期望它在所有平台上都能正常工作。
{
"scripts": {
"build": "esbuild --define:process.env.NODE_ENV=\\\"production\\\" app.js"
}
}
如果您仍然在不同的 shell 中遇到跨平台引号转义问题,您可能需要切换到使用 JavaScript API。在那里,您可以使用常规 JavaScript 语法来消除跨平台差异。
如果您正在寻找更高级形式的 define 功能,该功能可以将表达式替换为非常量内容(例如,将全局变量替换为 shim),您可能可以使用类似的 inject 功能来做到这一点。
#删除
这告诉 esbuild 在构建之前编辑您的源代码以删除某些结构。目前,可以删除两种可能的东西。
debugger
传递此标志会导致所有
debugger
语句 从输出中删除。这类似于流行的 UglifyJS 和 Terser JavaScript 压缩器中提供的drop_debugger: true
标志。JavaScript 的
debugger
语句会导致活动调试器将该语句视为自动配置的断点。当调试器打开时,包含此语句的代码将自动暂停。如果调试器未打开,则该语句将不执行任何操作。从您的代码中删除这些语句只是阻止调试器在您的代码运行时自动停止。您可以像这样删除
debugger
语句。
esbuild app.js --drop:debugger
import * as esbuild from 'esbuild'
await esbuild.build({
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
传递此标志会导致所有
console
API 调用 从输出中删除。这类似于流行的 UglifyJS 和 Terser JavaScript 压缩器中提供的drop_console: true
标志。警告:使用此标志可能会在您的代码中引入错误!此标志会删除整个调用表达式,包括所有调用参数。如果这些参数中的任何一个具有重要的副作用,使用此标志将改变您的代码的行为。在使用此标志时要非常小心。
如果您想删除控制台 API 调用而不删除具有副作用的参数(因此您不会引入错误),您应该将相关的 API 调用标记为 pure。例如,您可以使用
--pure:
将console.log console.log
标记为 pure。这将导致这些 API 调用在启用压缩时被安全地删除。您可以像这样删除
console
API 调用。
esbuild app.js --drop:console
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
#删除标签
这告诉 esbuild 在构建之前编辑您的源代码以删除具有特定标签名称的 带标签的语句。例如,考虑以下代码。
function example() {
DEV: doAnExpensiveCheck()
return normalCodePath()
}
如果您使用此选项删除所有名为 DEV
的标签,那么 esbuild 将为您提供以下内容。
function example() {
return normalCodePath();
}
您可以像这样配置此功能(这将删除 DEV
和 TEST
标签)。
esbuild app.js --drop-labels=DEV,TEST
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['app.js'],
dropLabels: ['DEV', 'TEST'],
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
DropLabels: []string{"DEV", "TEST"},
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
请注意,这不是唯一一种有条件地删除代码的方法。另一种更常见的方法是使用 define 功能将特定全局变量替换为布尔值。例如,考虑以下代码。
function example() {
DEV && doAnExpensiveCheck()
return normalCodePath()
}
如果您将 DEV
定义为 false
,那么 esbuild 将为您提供以下内容。
function example() {
return normalCodePath();
}
这与使用标签几乎相同。但是,与使用全局变量相比,使用标签来有条件地删除代码的优势在于,您不必担心全局变量未定义,因为有人忘记配置 esbuild 将其替换为其他内容。使用标签方法的一些缺点是,当标签未被删除时,它会使有条件地删除代码变得稍微难以阅读,并且它不适用于嵌套表达式中的嵌入代码。对于给定项目使用哪种方法取决于个人喜好。
#忽略注释
由于 JavaScript 是一种动态语言,因此编译器有时很难识别未使用的代码,因此社区开发了一些注释来帮助告诉编译器哪些代码应该被视为无副作用且可供删除。目前,esbuild 支持两种形式的副作用注释
在函数调用之前内联
/* @__PURE__ */
注释告诉 esbuild,如果结果值未被使用,则可以删除函数调用。有关更多信息,请参阅 pure API 选项。package.json
中的sideEffects
字段可用于告诉 esbuild 您的包中的哪些文件可以在所有来自该文件的导入最终未被使用时删除。这是来自 Webpack 的约定,许多发布到 npm 的库已经在其包定义中包含了此字段。您可以在 Webpack 的文档 中了解有关此字段的更多信息。
这些注释可能存在问题,因为编译器完全依赖于开发人员的准确性,而开发人员偶尔会发布带有错误注释的包。sideEffects
字段对于开发人员来说尤其容易出错,因为默认情况下,如果未使用任何导入,它会导致您的包中的所有文件都被视为死代码。如果您添加了一个包含副作用的新文件并忘记更新该字段,那么当人们尝试捆绑它时,您的包可能会出现故障。
这就是 esbuild 包含一种忽略副作用注释的方法的原因。只有在遇到由于意外从捆绑包中删除了必要的代码而导致捆绑包损坏的问题时,您才应该启用此功能
esbuild app.js --bundle --ignore-annotations
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
ignoreAnnotations: true,
outfile: 'out.js',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
IgnoreAnnotations: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
启用此功能意味着 esbuild 将不再尊重 /* @__PURE__ */
注释或 sideEffects
字段。但是,它仍然会对未使用的导入进行自动 树摇,因为这并不依赖于开发人员的注释。理想情况下,此标志只是一个临时的解决方法。您应该将这些问题报告给包的维护者以进行修复,因为它们表明包存在问题,并且它们也可能会让其他人绊倒。
#注入
支持:构建
此选项允许您自动将全局变量替换为来自另一个文件的导入。这对于将您无法控制的代码适应新环境来说是一个有用的工具。例如,假设您有一个名为 process-cwd-shim.js
的文件,它使用导出名称 process.cwd
导出一个垫片
// process-cwd-shim.js
let processCwdShim = () => ''
export { processCwdShim as 'process.cwd' }
// entry.js
console.log(process.cwd())
这旨在替换对 node 的 process.cwd()
函数的使用,以防止调用它的包在浏览器中运行时崩溃。您可以使用注入功能将对全局属性 process.cwd
的所有引用替换为来自该文件的导入
esbuild entry.js --inject:./process-cwd-shim.js --outfile=out.js
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['entry.js'],
inject: ['./process-cwd-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"},
Inject: []string{"./process-cwd-shim.js"},
Outfile: "out.js",
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
这将产生类似于以下内容的结果
// out.js
var processCwdShim = () => "";
console.log(processCwdShim());
您可以将注入功能视为类似于 define 功能,只是它将表达式替换为对文件的导入,而不是替换为常量,并且要替换的表达式是使用文件中的导出名称指定的,而不是使用 esbuild API 中的内联字符串。
#JSX 的自动导入
React(最初创建 JSX 语法的库)有一个名为 automatic
的模式,您不必 import
任何内容即可使用 JSX 语法。相反,JSX 到 JS 转换器将自动为您导入正确的 JSX 工厂函数。您可以使用 esbuild 的 jsx
设置启用 automatic
JSX 模式。如果您想要 JSX 的自动导入并且您使用的是足够新的版本的 React,那么您应该使用 automatic
JSX 模式。
但是,将 jsx
设置为 automatic
不幸地也意味着您正在使用高度特定于 React 的 JSX 转换,而不是默认的通用 JSX 转换。这意味着编写 JSX 工厂函数更加复杂,这也意味着 automatic
模式不适用于期望与标准 JSX 转换一起使用的库(包括旧版本的 React)。
您可以使用 esbuild 的注入功能在 JSX 转换未设置为 automatic
时自动导入 JSX 表达式的 工厂 和 片段。以下是一个可以注入以执行此操作的示例文件
const { createElement, Fragment } = require('react')
export {
createElement as 'React.createElement',
Fragment as 'React.Fragment',
}
此代码使用 React 库作为示例,但您也可以通过适当的更改将此方法与任何其他 JSX 库一起使用。
#注入没有导入的文件
您也可以将此功能与没有导出的文件一起使用。在这种情况下,注入的文件只是在输出的其余部分之前出现,就好像每个输入文件都包含 import
一样。由于 ECMAScript 模块的工作方式,这种注入仍然是“卫生的”,因为不同文件中的同名符号会被重命名,以避免它们相互冲突。
#有条件地注入文件
如果您想有条件地仅在实际使用导出时导入文件,则应将注入文件标记为没有副作用,方法是将其放在包中并在该包的 package.json
文件中添加 "sideEffects":
。此设置是来自 Webpack 的约定,esbuild 会尊重任何导入的文件,而不仅仅是使用 inject 的文件。
#保留名称
在 JavaScript 中,函数和类的 name
属性默认为源代码中的附近标识符。这些语法形式都将函数的 name
属性设置为 "fn"
function fn() {}
let fn = function() {};
fn = function() {};
let [fn = function() {}] = [];
let {fn = function() {}} = {};
[fn = function() {}] = [];
({fn = function() {}} = {});
但是,缩小 会重命名符号以减少代码大小,并且 捆绑 有时需要重命名符号以避免冲突。这会更改许多这些情况下的 name
属性的值。这通常没问题,因为 name
属性通常仅用于调试。但是,一些框架依赖于 name
属性进行注册和绑定。如果是这种情况,您可以启用此选项以保留原始的 name
值,即使在缩小的代码中也是如此
esbuild app.js --minify --keep-names
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
请注意,如果 target 已设置为不支持 esbuild 更改函数和类上的 name
属性的旧环境,则此功能不可用。对于不支持 ES6 的环境,情况就是这样。
#混淆属性
此设置允许您将正则表达式传递给 esbuild,以告诉 esbuild 自动重命名与该正则表达式匹配的所有属性。当您想要缩小代码中的某些属性名称以使生成的代码更小或以某种方式混淆代码的意图时,这很有用。
以下是一个使用正则表达式 _$
来混淆所有以下划线结尾的属性(例如 foo_
)的示例。这会将 print({
混淆为 print({
esbuild app.js --mangle-props=_$
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
仅混淆以下划线结尾的属性是一个合理的启发式方法,因为正常的 JS 代码通常不包含这样的标识符。浏览器 API 也不使用这种命名约定,因此这也避免了与浏览器 API 的冲突。如果您想避免混淆诸如 __defineGetter__
之类的名称,您可以考虑使用更复杂的正则表达式,例如 [^_]_$
(即必须以非下划线结尾,后跟下划线)。
这是一个单独的设置,而不是 minify 设置的一部分,因为它是一种不安全的转换,不适用于任意 JavaScript 代码。它仅在提供的正则表达式匹配您想要混淆的所有属性并且不匹配您不想混淆的任何属性时才有效。它也仅在您在任何情况下都不间接引用混淆的属性时才有效。例如,这意味着您不能使用 obj[prop]
来引用一个属性,其中 prop
是一个包含属性名称的字符串。具体来说,以下语法结构是唯一有资格进行属性混淆的结构
语法 | 示例 |
---|---|
点属性访问 | x.foo_ |
点可选链 | x?.foo_ |
对象属性 | x = { foo_: y } |
对象方法 | x = { foo_() {} } |
类字段 | class x { foo_ = y } |
类方法 | class x { foo_() {} } |
对象解构绑定 | let { foo_: x } = y |
对象解构赋值 | ({ foo_: x } = y) |
JSX 元素成员表达式 | <X.foo_></X.foo_> |
JSX 属性名称 | <X foo_={y} /> |
TypeScript 命名空间导出 | namespace x { export let foo_ = y } |
TypeScript 参数属性 | class x { constructor(public foo_) {} } |
使用此功能时,请记住,属性名称仅在单个 esbuild API 调用中始终如一地混淆,而不是跨 esbuild API 调用。每个 esbuild API 调用都会执行独立的属性混淆操作,因此由两个不同的 API 调用生成的输出文件可能会将相同的属性混淆为两个不同的名称,这会导致生成的代码行为不正确。
#带引号的属性
默认情况下,esbuild 不会修改字符串文字的内容。这意味着您可以通过将属性作为字符串引用来避免对单个属性进行属性混淆。但是,您必须始终对给定属性使用引号或不使用引号,才能使此方法起作用。例如,print({
将被混淆为 print({
,而 print({
不会被混淆。
如果您希望 esbuild 也混淆字符串文字的内容,您可以像这样显式启用该行为
esbuild app.js --mangle-props=_$ --mangle-quoted
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
启用此功能将使以下语法结构也有资格进行属性混淆
语法 | 示例 |
---|---|
带引号的属性访问 | x['foo_'] |
带引号的可选链 | x?.['foo_'] |
带引号的对象属性 | x = { 'foo_': y } |
带引号的对象方法 | x = { 'foo_'() {} } |
带引号的类字段 | class x { 'foo_' = y } |
带引号的类方法 | class x { 'foo_'() {} } |
带引号的对象解构绑定 | let { 'foo_': x } = y |
带引号的对象解构赋值 | ({ 'foo_': x } = y) |
in 左侧的字符串文字 |
'foo_' in x |
#混淆其他字符串
混淆 带引号的属性 仍然只混淆属性名称位置的字符串。有时您可能还需要混淆代码中任意其他位置的字符串中的属性名称。为此,您可以使用 /* @__KEY__ */
注释作为字符串的前缀,以告诉 esbuild 将字符串的内容视为可以混淆的属性名称。例如
let obj = {}
Object.defineProperty(
obj,
/* @__KEY__ */ 'foo_',
{ get: () => 123 },
)
console.log(obj.foo_)
这将导致字符串 'foo_'
的内容被混淆为属性名称(假设 属性混淆 已启用并且 foo_
有资格重命名)。/* @__KEY__ */
注释是来自 Terser 的约定,Terser 是一种流行的 JavaScript 缩小器,具有类似的属性混淆功能。
#防止重命名
如果您想排除某些属性不被混淆,您可以使用额外的设置来保留它们。例如,这使用正则表达式^__.*__$
来保留所有以两个下划线开头和结尾的属性,例如__foo__
esbuild app.js --mangle-props=_$ "--reserve-props=^__.*__$"
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
#持久化重命名决策
属性混淆功能的高级用法涉及将原始名称到混淆名称的映射存储在持久缓存中。启用后,所有混淆的属性重命名将在初始构建期间记录在缓存中。后续构建将重用存储在缓存中的重命名,并为任何新添加的属性添加额外的重命名。这有一些后果
您可以通过在将缓存传递给 esbuild 之前编辑缓存来自定义哪些混淆的属性被重命名。
缓存充当所有被混淆的属性的列表。您可以轻松地扫描它以查看是否存在任何意外的属性重命名。
您可以通过将重命名后的值设置为
false
而不是字符串来禁用对单个属性的混淆。这类似于保留属性设置,但它是针对每个属性的。您可以确保构建之间的一致重命名(例如,主线程文件和 Web 工作器,或库和插件)。如果没有此功能,每个构建都会执行独立的重命名操作,并且混淆的属性名称可能不一致。
例如,考虑以下输入文件
console.log({
someProp_: 1,
customRenaming_: 2,
disabledRenaming_: 3
});
如果我们希望customRenaming_
被重命名为cR_
,并且我们不希望disabledRenaming_
被重命名,我们可以将以下混淆缓存 JSON 传递给 esbuild
{
"customRenaming_": "cR_",
"disabledRenaming_": false
}
混淆缓存 JSON 可以像这样传递给 esbuild
esbuild app.js --mangle-props=_$ --mangle-cache=cache.json
import * as esbuild from 'esbuild'
let result = await esbuild.build({
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)
}
启用属性命名后,将生成以下输出文件
console.log({
a: 1,
cR_: 2,
disabledRenaming_: 3
});
以及以下更新的混淆缓存
{
"customRenaming_": "cR_",
"disabledRenaming_": false,
"someProp_": "a"
}
#压缩
启用后,生成的代码将被压缩而不是漂亮打印。压缩后的代码通常等同于未压缩的代码,但体积更小,这意味着它下载速度更快,但更难调试。通常您在生产中压缩代码,但在开发中不压缩。
在 esbuild 中启用压缩如下所示
echo 'fn = obj => { return obj.x }' | esbuild --minify
fn=n=>n.x;
import * as esbuild from 'esbuild'var js = 'fn = obj => { return obj.x }'
(await esbuild.transform(js, {
minify: true,
})).code
'fn=n=>n.x;\n'
package main
import "fmt"
import "github.com/evanw/esbuild/pkg/api"
func main() {
js := "fn = obj => { return obj.x }"
result := api.Transform(js, api.TransformOptions{
MinifyWhitespace: true,
MinifyIdentifiers: true,
MinifySyntax: true,
})
if len(result.Errors) == 0 {
fmt.Printf("%s", result.Code)
}
}
此选项组合执行三项独立操作:它删除空白,它将您的语法重写为更紧凑的形式,并且它将局部变量重命名为更短的名称。通常您希望执行所有这些操作,但如果需要,也可以单独启用这些选项。
echo 'fn = obj => { return obj.x }' | esbuild --minify-whitespace
fn=obj=>{return obj.x};
echo 'fn = obj => { return obj.x }' | esbuild --minify-identifiers
fn = (n) => {
return n.x;
};
echo 'fn = obj => { return obj.x }' | esbuild --minify-syntax
fn = (obj) => obj.x;
import * as esbuild from 'esbuild'var js = 'fn = obj => { return obj.x }'
(await esbuild.transform(js, {
minifyWhitespace: true,
})).code
'fn=obj=>{return obj.x};\n'
(await esbuild.transform(js, {
minifyIdentifiers: true,
})).code
'fn = (n) => {\n return n.x;\n};\n'
(await esbuild.transform(js, {
minifySyntax: true,
})).code
'fn = (obj) => obj.x;\n'
package main
import "fmt"
import "github.com/evanw/esbuild/pkg/api"
func main() {
css := "div { color: yellow }"
result1 := api.Transform(css, api.TransformOptions{
Loader: api.LoaderCSS,
MinifyWhitespace: true,
})
if len(result1.Errors) == 0 {
fmt.Printf("%s", result1.Code)
}
result2 := api.Transform(css, api.TransformOptions{
Loader: api.LoaderCSS,
MinifyIdentifiers: true,
})
if len(result2.Errors) == 0 {
fmt.Printf("%s", result2.Code)
}
result3 := api.Transform(css, api.TransformOptions{
Loader: api.LoaderCSS,
MinifySyntax: true,
})
if len(result3.Errors) == 0 {
fmt.Printf("%s", result3.Code)
}
}
这些相同的概念也适用于 CSS,而不仅仅是 JavaScript
echo 'div { color: yellow }' | esbuild --loader=css --minify
div{color:#ff0}
import * as esbuild from 'esbuild'var css = 'div { color: yellow }'
(await esbuild.transform(css, {
loader: 'css',
minify: true,
})).code
'div{color:#ff0}\n'
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 压缩工具的压缩输出大小。 此基准 提供了不同压缩器之间输出大小比较的示例。虽然 esbuild 并非在所有情况下都是最佳的 JavaScript 压缩器(并且没有尝试成为最佳),但它努力为大多数代码生成与专用压缩工具大小相差不到几个百分点的压缩输出,当然,比其他工具快得多。
#注意事项
在使用 esbuild 作为压缩器时,请记住以下几点
启用压缩时,您可能还应该设置目标选项。默认情况下,esbuild 利用现代 JavaScript 功能来使您的代码更小。例如,
a ===
可以压缩为undefined || a === null ? 1 : a a ?? 1
。如果您不希望 esbuild 在压缩时利用现代 JavaScript 功能,您应该使用更旧的语言目标,例如--target=es6
。字符转义序列
\n
将在 JavaScript 模板字面量中被替换为换行符。如果目标 支持字符串字面量,并且这样做会导致更小的输出,则字符串字面量也将被转换为模板字面量。这不是错误。 压缩意味着您要求更小的输出,而转义序列\n
占用两个字节,而换行符占用一个字节。默认情况下,esbuild 不会压缩顶层声明的名称。这是因为 esbuild 不知道您将如何处理输出。您可能将压缩后的代码注入到其他代码的中间,在这种情况下,压缩顶层声明名称将是不安全的。设置输出格式(或启用捆绑,如果您没有设置输出格式,它会为您选择一个输出格式)告诉 esbuild 输出将在其自己的范围内运行,这意味着现在可以安全地压缩顶层声明名称。
压缩并非对所有 JavaScript 代码都安全。这对 esbuild 以及其他流行的 JavaScript 压缩器(例如 terser)都是如此。特别是,esbuild 并非旨在保留对函数调用
.toString()
的返回值。这样做的原因是,如果所有函数内部的所有代码都必须逐字保留,压缩几乎不会做任何事情,并且实际上毫无用处。但是,这意味着依赖于.toString()
的返回值的 JavaScript 代码在压缩后可能会出现故障。例如,AngularJS 框架中的一些模式在代码被压缩后会失效,因为 AngularJS 使用.toString()
来读取函数的参数名称。解决方法是使用显式注释代替。默认情况下,esbuild 不会保留函数和类对象上
.name
的值。这是因为大多数代码不依赖于此属性,使用更短的名称是一个重要的尺寸优化。但是,一些代码确实依赖于.name
属性进行注册和绑定。如果您需要依赖此属性,您应该启用保留名称选项。使用某些 JavaScript 功能可能会禁用 esbuild 的许多优化,包括压缩。具体来说,使用直接
eval
和/或with
语句会阻止 esbuild 将标识符重命名为更短的名称,因为这些功能会导致标识符绑定在运行时而不是编译时发生。这几乎总是无意的,只发生在人们不知道直接eval
是什么以及为什么它不好。如果您正在考虑编写类似这样的代码
// Direct eval (will disable minification for the whole file) let result = eval(something)
您应该改为像这样编写您的代码,以便您的代码可以被压缩
// Indirect eval (has no effect on the surrounding code) let result = (0, eval)(something)
有关直接
eval
的后果以及可用替代方案的更多信息,请点击这里。esbuild 中的压缩算法尚未进行高级代码优化。特别是,以下代码优化对于 JavaScript 代码是可能的,但 esbuild 并没有执行(不是详尽的列表)
- 函数体内的死代码消除
- 函数内联
- 跨语句常量传播
- 对象形状建模
- 分配下沉
- 方法去虚拟化
- 符号执行
- JSX 表达式提升
- TypeScript 枚举检测和内联
如果您的代码使用需要某些形式的代码优化才能紧凑的模式,或者如果您正在寻找最适合您的用例的 JavaScript 压缩算法,您应该考虑使用其他工具。一些实现这些高级代码优化的一些工具的示例包括 Terser 和 Google Closure Compiler。
#纯
各种 JavaScript 工具使用一种约定,其中包含 /* @__PURE__ */
或 /* #__PURE__ */
的特殊注释在新的或调用表达式之前表示该表达式可以被删除,如果结果值未被使用。它看起来像这样
let button = /* @__PURE__ */ React.createElement(Button, null);
此信息在捆绑器(如 esbuild)在树摇动(也称为死代码删除)期间使用,以在捆绑器无法自行证明删除是安全的(由于 JavaScript 代码的动态特性)的情况下,跨模块边界执行对未使用的导入的细粒度删除。
请注意,虽然注释说“纯”,但它令人困惑地没有表示被调用的函数是纯的。例如,它不表示可以缓存对该函数的重复调用。这个名称本质上只是“如果未使用则可以删除”的抽象简写。
esbuild 中会自动将某些表达式(如 JSX 和某些内置全局变量)注释为 /* @__PURE__ */
。您还可以配置其他全局变量以标记为 /* @__PURE__ */
。例如,您可以将全局 document.
函数标记为这样,以便在捆绑包被压缩时,只要结果没有被使用,它就会自动从捆绑包中删除。
值得一提的是,注释的效果只扩展到调用本身,而不扩展到参数。即使启用压缩,具有副作用的参数也会被保留
echo 'document.createElement(elemName())' | esbuild --pure:document.createElement
/* @__PURE__ */ document.createElement(elemName());
echo 'document.createElement(elemName())' | esbuild --pure:document.createElement --minify
elemName();
import * as esbuild from 'esbuild'let js = 'document.createElement(elemName())'
(await esbuild.transform(js, {
pure: ['document.createElement'],
})).code
'/* @__PURE__ */ document.createElement(elemName());\n'
(await esbuild.transform(js, {
pure: ['document.createElement'],
minify: true,
})).code
'elemName();\n'
package main
import "fmt"
import "github.com/evanw/esbuild/pkg/api"
func main() {
js := "document.createElement(elemName())"
result1 := api.Transform(js, api.TransformOptions{
Pure: []string{"document.createElement"},
})
if len(result1.Errors) == 0 {
fmt.Printf("%s", result1.Code)
}
result2 := api.Transform(js, api.TransformOptions{
Pure: []string{"document.createElement"},
MinifySyntax: true,
})
if len(result2.Errors) == 0 {
fmt.Printf("%s", result2.Code)
}
}
请注意,如果您尝试删除对 console
API 方法(如 console.log
)的所有调用,并且还希望删除具有副作用的参数的求值,则为此提供了一个特殊情况:您可以使用drop 功能,而不是将 console
API 调用标记为纯的。但是,此机制特定于 console
API,不适用于其他调用表达式。
#树摇动
树摇动是 JavaScript 社区用来表示死代码消除的术语,这是一种常见的编译器优化,它会自动删除无法访问的代码。在 esbuild 中,此术语专门指声明级死代码删除。
树摇动最容易用一个例子来解释。考虑以下文件。有一个被使用的函数和一个未被使用的函数
// 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 的树摇动实现依赖于使用 ECMAScript 模块 import
和 export
语句。它不适用于 CommonJS 模块。npm 上的许多包都包含这两种格式,esbuild 尝试默认选择与树摇动一起使用的格式。您可以使用主字段 和/或条件 选项来自定义 esbuild 选择的格式,具体取决于包。
默认情况下,树摇动仅在启用捆绑或输出格式 设置为 iife
时启用,否则树摇动将被禁用。您可以通过将其设置为 true
来强制启用树摇动
esbuild app.js --tree-shaking=true
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
您也可以通过将其设置为 false
来强制禁用树摇动
esbuild app.js --tree-shaking=false
import * as esbuild from 'esbuild'
await esbuild.build({
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 仅将代码视为可删除的死代码,如果它可以确定没有隐藏的副作用。例如,原始字面量(如 12.34
和 "abcd"
)是无副作用的,可以被删除,而表达式(如 "ab" + cd
和 foo.bar
)不是无副作用的(连接字符串会调用 toString()
,它可能具有副作用,成员访问可能会调用 getter,它也可能具有副作用)。即使引用全局标识符也被认为是副作用,因为它会在没有该名称的全局变量的情况下抛出 ReferenceError
。以下是一个示例
// These are considered side-effect free
let a = 12.34;
let b = "abcd";
let c = { a: a };
// These are not considered side-effect free
// since they could cause some code to run
let x = "ab" + cd;
let y = foo.bar;
let z = { [x]: x };
有时,即使无法自动确定代码没有副作用,也希望允许对某些代码进行树状抖动。这可以通过 纯注释 来实现,它告诉 esbuild 信任代码作者,注释的代码中没有副作用。注释是 /* @__PURE__ */
,只能放在新的或调用表达式之前。您可以注释一个立即调用的函数表达式,并在函数体中放置任意副作用。
// This is considered side-effect free due to
// the annotation, and will be removed if unused
let gammaTable = /* @__PURE__ */ (() => {
// Side-effect detection is skipped in here
let table = new Uint8Array(256);
for (let i = 0; i < 256; i++)
table[i] = Math.pow(i / 255, 2.2) * 255;
return table;
})();
虽然 /* @__PURE__ */
只能用于调用表达式这一事实有时会使代码更冗长,但这种语法的一个主要优点是它可以在 JavaScript 生态系统中的许多其他工具中移植,包括流行的 UglifyJS 和 Terser JavaScript 压缩器(它们被其他主要工具使用,包括 Webpack 和 Parcel)。
请注意,注释会导致 esbuild 假设注释的代码没有副作用。如果注释错误,并且代码实际上确实具有重要的副作用,则这些注释会导致代码出错。如果您正在捆绑带有错误注释的第三方代码,您可能需要启用 忽略注释 以确保捆绑的代码正确。
#源映射
#源根目录
此功能仅在启用 源映射 时才相关。它允许您设置源映射中 sourceRoot
字段的值,该字段指定源映射中所有其他路径的相对路径。如果此字段不存在,则源映射中的所有路径都将被解释为相对于包含源映射的目录。
您可以像这样配置 sourceRoot
esbuild app.js --sourcemap --source-root=https://raw.githubusercontent.com/some/repo/v1.2.3/
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
#源文件
此选项在使用没有文件名的输入时设置文件名。这发生在使用 transform API 时,以及在使用 build API 和 stdin 时。配置的文件名会反映在错误消息和源映射中。如果未配置,则文件名默认为 <stdin>
。它可以像这样配置
cat app.js | esbuild --sourcefile=example.js --sourcemap
import * as esbuild from 'esbuild'
import fs from 'node:fs'
let js = fs.readFileSync('app.js', 'utf8')
let result = await esbuild.transform(js, {
sourcefile: 'example.js',
sourcemap: 'inline',
})
console.log(result.code)
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 %s", result.Code)
}
}
#源映射
源映射可以使调试代码更容易。它们对必要的信息进行编码,以便从生成的输出文件中的行/列偏移量转换回相应的原始输入文件中的行/列偏移量。如果您生成的代码与原始代码有很大不同(例如,您的原始代码是 TypeScript 或您启用了 压缩),这将非常有用。如果您更喜欢在浏览器的开发者工具中查看单个文件而不是一个大型捆绑文件,这也很有用。
请注意,源映射输出同时支持 JavaScript 和 CSS,并且相同的选项适用于两者。下面所有关于 .js
文件的内容也同样适用于 .css
文件。
有四种不同的源映射生成模式
-
linked
此模式意味着源映射将生成到一个单独的
.js.map
输出文件中,该文件与.js
输出文件并排,并且.js
输出文件包含一个特殊的//# sourceMappingURL=
注释,指向.js.map
输出文件。这样,浏览器就知道在您打开调试器时,在哪里可以找到给定文件的源映射。像这样使用linked
源映射模式
esbuild app.ts --sourcemap --outfile=out.js
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
-
external
此模式意味着源映射将生成到一个单独的
.js.map
输出文件中,该文件与.js
输出文件并排,但与linked
模式不同,.js
输出文件不包含//# sourceMappingURL=
注释。像这样使用external
源映射模式
esbuild app.ts --sourcemap=external --outfile=out.js
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
-
inline
此模式意味着源映射将作为
//# sourceMappingURL=
注释中的 base64 负载附加到.js
输出文件的末尾。不会生成额外的.js.map
输出文件。请记住,源映射通常非常大,因为它们包含所有原始源代码,因此您通常不希望发送包含inline
源映射的代码。要从源映射中删除源代码(仅保留文件名和行/列映射),请使用 源代码内容 选项。像这样使用inline
源映射模式
esbuild app.ts --sourcemap=inline --outfile=out.js
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
-
两者
此模式是
inline
和external
的组合。源映射将内联附加到.js
输出文件的末尾,并且同一源映射的另一个副本将写入一个单独的.js.map
输出文件中,该文件与.js
输出文件并排。像这样使用both
源映射模式
esbuild app.ts --sourcemap=both --outfile=out.js
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
build API 支持上面列出的所有四种源映射模式,但 transform API 不支持 linked
模式。这是因为从 transform API 返回的输出没有关联的文件名。如果您希望 transform API 的输出包含源映射注释,您可以自己附加一个。此外,transform API 的 CLI 形式仅支持 inline
模式,因为输出写入 stdout,因此无法生成多个输出文件。
如果您想“窥视内部”以了解源映射的作用(或调试源映射问题),您可以将相关输出文件和关联的源映射上传到此处:源映射可视化。
#使用源映射
在浏览器中,只要启用了源映射设置,浏览器开发者工具应该会自动获取源映射。请注意,浏览器仅使用源映射来更改堆栈跟踪的显示方式,当它们被记录到控制台中时。堆栈跟踪本身不会被修改,因此在您的代码中检查 error.
仍然会给出包含已编译代码的未映射堆栈跟踪。以下是在浏览器开发者工具中启用此设置的方法
- Chrome:⚙ → 启用 JavaScript 源映射
- Safari:⚙ → 源代码 → 启用源映射
- Firefox:··· → 启用源映射
在 node 中,从 版本 v12.12.0 开始,源映射得到原生支持。此功能默认情况下处于禁用状态,但可以使用标志启用。与浏览器不同,node 中的实际堆栈跟踪也会被修改,因此在您的代码中检查 error.
会给出包含原始源代码的映射堆栈跟踪。以下是在 node 中启用此设置的方法(--enable-
标志必须位于脚本文件名之前)
node --enable-source-maps app.js
#源代码内容
源映射 是使用 版本 3 的源映射格式生成的,该格式是迄今为止最广泛支持的变体。每个源映射看起来都像这样
{
"version": 3,
"sources": ["bar.js", "foo.js"],
"sourcesContent": ["bar()", "foo()\nimport './bar'"],
"mappings": ";AAAA;;;ACAA;",
"names": []
}
sourcesContent
字段是一个可选字段,它包含所有原始源代码。这对于调试很有帮助,因为它意味着原始源代码将在调试器中可用。
但是,在某些情况下不需要它。例如,如果您只是在生产环境中使用源映射来生成包含原始文件名的堆栈跟踪,则不需要原始源代码,因为没有涉及调试器。在这种情况下,省略 sourcesContent
字段以使源映射更小可能是可取的
esbuild --bundle app.js --sourcemap --sources-content=false
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
#构建元数据
#分析
支持:构建
使用分析功能会生成一个关于捆绑包内容的易于阅读的报告
esbuild --bundle example.jsx --outfile=out.js --minify --analyze out.js 27.6kb 100.0% ├ node_modules/react-dom/cjs/react-dom-server.browser.production.min.js 19.2kb 69.8% ├ node_modules/react/cjs/react.production.min.js 5.9kb 21.4% ├ 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% ...
import * as esbuild from 'esbuild'
let result = await esbuild.build({
entryPoints: ['example.jsx'],
outfile: 'out.js',
minify: true,
metafile: true,
})
console.log(await esbuild.analyzeMetafile(result.metafile))
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)
}
fmt.Printf("%s", api.AnalyzeMetafile(result.Metafile, api.AnalyzeMetafileOptions{}))
}
该信息显示了哪些输入文件最终出现在每个输出文件中,以及它们最终占用了输出文件的百分比。如果您需要更多信息,可以启用“详细”模式。这目前显示了从入口点到每个输入文件的导入路径,它告诉您为什么将给定输入文件包含在捆绑包中
esbuild --bundle example.jsx --outfile=out.js --minify --analyze=verbose out.js ─────────────────────────────────────────────────────────────────── 27.6kb ─ 100.0% ├ node_modules/react-dom/cjs/react-dom-server.browser.production.min.js ─ 19.2kb ── 69.8% │ └ node_modules/react-dom/server.browser.js │ └ example.jsx ├ node_modules/react/cjs/react.production.min.js ───────────────────────── 5.9kb ── 21.4% │ └ 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 ...
import * as esbuild from 'esbuild'
let result = await esbuild.build({
entryPoints: ['example.jsx'],
outfile: 'out.js',
minify: true,
metafile: true,
})
console.log(await esbuild.analyzeMetafile(result.metafile, {
verbose: true,
}))
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)
}
fmt.Printf("%s", api.AnalyzeMetafile(result.Metafile, api.AnalyzeMetafileOptions{
Verbose: true,
}))
}
此分析只是对可以在 元文件 中找到的信息的可视化。如果此分析不完全适合您的需求,欢迎您使用元文件中的信息构建自己的可视化。
请注意,此格式化的分析摘要旨在供人类使用,而不是机器使用。具体格式可能会随着时间的推移而改变,这可能会破坏尝试解析它的任何工具。您不应该编写一个工具来解析这些数据。您应该使用 JSON 元数据文件 中的信息。此可视化中的所有内容都来自 JSON 元数据,因此您不会因为不解析 esbuild 的格式化分析摘要而丢失任何信息。
#元文件
支持:构建
此选项告诉 esbuild 以 JSON 格式生成有关构建的一些元数据。以下示例将元数据放在名为 meta.json
的文件中
esbuild app.js --bundle --metafile=meta.json --outfile=out.js
import * as esbuild from 'esbuild'
import fs from 'node:fs'
let result = await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
metafile: true,
outfile: 'out.js',
})
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)
}
然后,其他工具可以分析这些数据。对于交互式可视化,您可以使用 esbuild 自己的 捆绑大小分析器。对于快速文本分析,您可以使用 esbuild 的内置 分析 功能。或者,您可以编写自己的分析,它使用这些信息。
元数据 JSON 格式如下所示(使用 TypeScript 接口描述)
interface Metafile {
inputs: {
[path: string]: {
bytes: number
imports: {
path: string
kind: string
external?: boolean
original?: string
with?: Record<string, string>
}[]
format?: string
with?: Record<string, string>
}
}
outputs: {
[path: string]: {
bytes: number
inputs: {
[path: string]: {
bytesInOutput: number
}
}
imports: {
path: string
kind: string
external?: boolean
}[]
exports: string[]
entryPoint?: string
cssBundle?: string
}
}
}
#日志记录
#颜色
此选项启用或禁用 esbuild 写入终端 stderr 文件描述符的错误和警告消息中的颜色。默认情况下,如果 stderr 是 TTY 会话,则自动启用颜色,否则自动禁用。esbuild 中的彩色输出如下所示
▲ [WARNING] The "typeof" operator will never evaluate to "null" [impossible-typeof] 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 and leave the unresolved path in the bundle.
通过将 color 设置为 true
,可以强制启用彩色输出。如果您将 esbuild 的 stderr 输出管道到 TTY 本身,这将很有用
echo 'typeof x == "null"' | esbuild --color=true 2> stderr.txt
import * as esbuild from 'esbuild'
let js = 'typeof x == "null"'
await esbuild.transform(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)
}
}
彩色输出也可以设置为 false
以禁用颜色。
#格式化消息
此 API 调用可用于使用 esbuild 本身使用的相同格式将 build API 和 transform API 返回的日志错误和警告格式化为字符串。如果您想自定义 esbuild 的日志记录方式,例如在打印日志消息之前处理它们或将它们打印到控制台以外的位置,这将很有用。以下是一个示例
import * as esbuild from 'esbuild'
let formatted = await esbuild.formatMessages([
{
text: 'This is an error',
location: {
file: 'app.js',
line: 10,
column: 4,
length: 3,
lineText: 'let foo = bar',
},
},
], {
kind: 'error',
color: false,
terminalWidth: 100,
})
console.log(formatted.join('\n'))
package main
import "fmt"
import "github.com/evanw/esbuild/pkg/api"
import "strings"
func main() {
formatted := api.FormatMessages([]api.Message{
{
Text: "This is an error",
Location: &api.Location{
File: "app.js",
Line: 10,
Column: 4,
Length: 3,
LineText: "let foo = bar",
},
},
}, api.FormatMessagesOptions{
Kind: api.ErrorMessage,
Color: false,
TerminalWidth: 100,
})
fmt.Printf("%s", strings.Join(formatted, "\n"))
}
#选项
以下选项可以提供以控制格式
interface FormatMessagesOptions {
kind: 'error' | 'warning';
color?: boolean;
terminalWidth?: number;
}
type FormatMessagesOptions struct {
Kind MessageKind
Color bool
TerminalWidth int
}
种类
控制这些日志消息是作为错误还是警告打印。
颜色
如果为
true
,则包含 Unix 风格的终端转义码以进行彩色输出。终端宽度
提供一个正值以换行长行,以使它们不会溢出提供的列宽。提供
0
以禁用换行。
#日志级别
可以更改日志级别以阻止 esbuild 将警告和/或错误消息打印到终端。六个日志级别是
静默
不显示任何日志输出。这是使用 JS transform API 时的默认日志级别。错误
仅显示错误。警告
仅显示警告和错误。这是使用 JS build API 时的默认日志级别。信息
显示警告、错误和输出文件摘要。这是使用 CLI 时的默认日志级别。调试
记录info
中的所有内容以及一些可能有助于您调试损坏捆绑包的附加消息。此日志级别会影响性能,并且某些消息可能是误报,因此默认情况下不会显示这些信息。详细
这会生成大量日志消息,并被添加用于调试文件系统驱动程序的问题。它不适合一般使用。
可以像这样设置日志级别
echo 'typeof x == "null"' | esbuild --log-level=error
import * as esbuild from 'esbuild'
let js = 'typeof x == "null"'
await esbuild.transform(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 命令提示符。它还避免了意外地将整个滚动缓冲区用于滚动缓冲区有限的终端模拟器。
日志限制可以更改为另一个值,也可以通过将其设置为零来完全禁用。这将显示所有日志消息。
esbuild app.js --log-limit=0
import * as esbuild from 'esbuild'
await esbuild.build({
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)
}
}
#日志覆盖
此功能允许您更改单个日志消息类型的日志级别。您可以使用它来静默特定类型的警告,启用默认情况下未启用的其他警告,甚至将警告转换为错误。
例如,当针对较旧的浏览器时,esbuild 会自动将使用对这些浏览器来说过于新的功能的正则表达式字面量转换为 new
调用,以允许生成的代码在不被浏览器视为语法错误的情况下运行。但是,如果您没有为 RegExp
添加 polyfill,这些调用仍然会在运行时抛出异常,因为该正则表达式语法仍然不受支持。如果您希望 esbuild 在您使用较新的不受支持的正则表达式语法时生成警告,您可以像这样操作。
esbuild app.js --log-override:unsupported-regexp=warning --target=chrome50
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['app.js'],
logOverride: {
'unsupported-regexp': 'warning',
},
target: 'chrome50',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
LogOverride: map[string]api.LogLevel{
"unsupported-regexp": api.LogLevelWarning,
},
Engines: []api.Engine{
{Name: api.EngineChrome, Version: "50"},
},
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
每种消息类型的日志级别可以覆盖为 日志级别 设置支持的任何值。所有当前可用的消息类型列在下面(单击每个消息类型以查看示例日志消息)。
-
JS
assign-to-constant
▲ [WARNING] This assignment will throw because "foo" is a constant [assign-to-constant] example.js:1:15: 1 │ const foo = 1; foo = 2 ╵ ~~~ The symbol "foo" was declared a constant here: example.js:1:6: 1 │ const foo = 1; foo = 2 ╵ ~~~
assign-to-import
▲ [WARNING] This assignment will throw because "foo" is an import [assign-to-import] example.js:1:23: 1 │ import foo from "foo"; foo = null ╵ ~~~ Imports are immutable in JavaScript. To modify the value of this import, you must export a setter function in the imported file (e.g. "setFoo") and then import and call that function here instead.
call-import-namespace
▲ [WARNING] Calling "foo" will crash at run-time because it's an import namespace object, not a function [call-import-namespace] example.js:1:28: 1 │ import * as foo from "foo"; foo() ╵ ~~~ Consider changing "foo" to a default import instead: example.js:1:7: 1 │ import * as foo from "foo"; foo() │ ~~~~~~~~ ╵ foo
commonjs-variable-in-esm
▲ [WARNING] The CommonJS "exports" variable is treated as a global variable in an ECMAScript module and may not work as expected [commonjs-variable-in-esm] example.js:1:0: 1 │ exports.foo = 1; export let bar = 2 ╵ ~~~~~~~ This file is considered to be an ECMAScript module because of the "export" keyword here: example.js:1:17: 1 │ exports.foo = 1; export let bar = 2 ╵ ~~~~~~
delete-super-property
▲ [WARNING] Attempting to delete a property of "super" will throw a ReferenceError [delete-super-property] example.js:1:42: 1 │ class Foo extends Object { foo() { delete super.foo } } ╵ ~~~~~
duplicate-case
▲ [WARNING] This case clause will never be evaluated because it duplicates an earlier case clause [duplicate-case] example.js:1:33: 1 │ switch (foo) { case 1: return 1; case 1: return 2 } ╵ ~~~~ The earlier case clause is here: example.js:1:15: 1 │ switch (foo) { case 1: return 1; case 1: return 2 } ╵ ~~~~
duplicate-object-key
▲ [WARNING] Duplicate key "bar" in object literal [duplicate-object-key] example.js:1:16: 1 │ foo = { bar: 1, bar: 2 } ╵ ~~~ The original key "bar" is here: example.js:1:8: 1 │ foo = { bar: 1, bar: 2 } ╵ ~~~
empty-import-meta
▲ [WARNING] "import.meta" is not available in the configured target environment ("chrome50") and will be empty [empty-import-meta] example.js:1:6: 1 │ foo = import.meta ╵ ~~~~~~~~~~~
equals-nan
▲ [WARNING] Comparison with NaN using the "!==" operator here is always true [equals-nan] example.js:1:24: 1 │ foo = foo.filter(x => x !== NaN) ╵ ~~~ Floating-point equality is defined such that NaN is never equal to anything, so "x === NaN" always returns false. You need to use "Number.isNaN(x)" instead to test for NaN.
equals-negative-zero
▲ [WARNING] Comparison with -0 using the "!==" operator will also match 0 [equals-negative-zero] example.js:1:28: 1 │ foo = foo.filter(x => x !== -0) ╵ ~~ Floating-point equality is defined such that 0 and -0 are equal, so "x === -0" returns true for both 0 and -0. You need to use "Object.is(x, -0)" instead to test for -0.
equals-new-object
▲ [WARNING] Comparison using the "!==" operator here is always true [equals-new-object] example.js:1:24: 1 │ foo = foo.filter(x => x !== []) ╵ ~~~ Equality with a new object is always false in JavaScript because the equality operator tests object identity. You need to write code to compare the contents of the object instead. For example, use "Array.isArray(x) && x.length === 0" instead of "x === []" to test for an empty array.
html-comment-in-js
▲ [WARNING] Treating "<!--" as the start of a legacy HTML single-line comment [html-comment-in-js] example.js:1:0: 1 │ <!-- comment --> ╵ ~~~~
impossible-typeof
▲ [WARNING] The "typeof" operator will never evaluate to "null" [impossible-typeof] example.js:1:32: 1 │ foo = foo.map(x => 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.
indirect-require
▲ [WARNING] Indirect calls to "require" will not be bundled [indirect-require] example.js:1:8: 1 │ let r = require, fs = r("fs") ╵ ~~~~~~~
private-name-will-throw
▲ [WARNING] Writing to getter-only property "#foo" will throw [private-name-will-throw] example.js:1:39: 1 │ class Foo { get #foo() {} bar() { this.#foo++ } } ╵ ~~~~
semicolon-after-return
▲ [WARNING] The following expression is not returned because of an automatically-inserted semicolon [semicolon-after-return] example.js:1:6: 1 │ return ╵ ^
suspicious-boolean-not
▲ [WARNING] Suspicious use of the "!" operator inside the "in" operator [suspicious-boolean-not] example.js:1:4: 1 │ if (!foo in bar) { │ ~~~~ ╵ (!foo) The code "!x in y" is parsed as "(!x) in y". You need to insert parentheses to get "!(x in y)" instead.
this-is-undefined-in-esm
▲ [WARNING] Top-level "this" will be replaced with undefined since this file is an ECMAScript module [this-is-undefined-in-esm] example.js:1:0: 1 │ this.foo = 1; export let bar = 2 │ ~~~~ ╵ undefined This file is considered to be an ECMAScript module because of the "export" keyword here: example.js:1:14: 1 │ this.foo = 1; export let bar = 2 ╵ ~~~~~~
unsupported-dynamic-import
▲ [WARNING] This "import" expression will not be bundled because the argument is not a string literal [unsupported-dynamic-import] example.js:1:0: 1 │ import(foo) ╵ ~~~~~~
unsupported-jsx-comment
▲ [WARNING] Invalid JSX factory: 123 [unsupported-jsx-comment] example.jsx:1:8: 1 │ // @jsx 123 ╵ ~~~
unsupported-regexp
▲ [WARNING] The regular expression flag "d" is not available in the configured target environment ("chrome50") [unsupported-regexp] example.js:1:3: 1 │ /./d ╵ ^ This regular expression literal has been converted to a "new RegExp()" constructor to avoid generating code with a syntax error. However, you will need to include a polyfill for "RegExp" for your code to have the correct behavior at run-time.
unsupported-require-call
▲ [WARNING] This call to "require" will not be bundled because the argument is not a string literal [unsupported-require-call] example.js:1:0: 1 │ require(foo) ╵ ~~~~~~~
-
CSS
css-syntax-error
▲ [WARNING] Expected identifier but found "]" [css-syntax-error] example.css:1:4: 1 │ div[] { ╵ ^
invalid-@charset
▲ [WARNING] "@charset" must be the first rule in the file [invalid-@charset] example.css:1:19: 1 │ div { color: red } @charset "UTF-8"; ╵ ~~~~~~~~ This rule cannot come before a "@charset" rule example.css:1:0: 1 │ div { color: red } @charset "UTF-8"; ╵ ^
invalid-@import
▲ [WARNING] All "@import" rules must come first [invalid-@import] example.css:1:19: 1 │ div { color: red } @import "foo.css"; ╵ ~~~~~~~ This rule cannot come before an "@import" rule example.css:1:0: 1 │ div { color: red } @import "foo.css"; ╵ ^
invalid-@layer
▲ [WARNING] "initial" cannot be used as a layer name [invalid-@layer] example.css:1:7: 1 │ @layer initial { ╵ ~~~~~~~
invalid-calc
▲ [WARNING] "-" can only be used as an infix operator, not a prefix operator [invalid-calc] example.css:1:20: 1 │ div { z-index: calc(-(1+2)); } ╵ ^ ▲ [WARNING] The "+" operator only works if there is whitespace on both sides [invalid-calc] example.css:1:23: 1 │ div { z-index: calc(-(1+2)); } ╵ ^
js-comment-in-css
▲ [WARNING] Comments in CSS use "/* ... */" instead of "//" [js-comment-in-css] example.css:1:0: 1 │ // comment ╵ ~~
undefined-composes-from
▲ [WARNING] The value of "zoom" in the "foo" class is undefined [undefined-composes-from] example.module.css:1:1: 1 │ .foo { composes: bar from "lib.module.css"; zoom: 1; } ╵ ~~~ The first definition of "zoom" is here: lib.module.css:1:7: 1 │ .bar { zoom: 2 } ╵ ~~~~ The second definition of "zoom" is here: example.module.css:1:44: 1 │ .foo { composes: bar from "lib.module.css"; zoom: 1; } ╵ ~~~~ The specification of "composes" does not define an order when class declarations from separate files are composed together. The value of the "zoom" property for "foo" may change unpredictably as the code is edited. Make sure that all definitions of "zoom" for "foo" are in a single file.
unsupported-@charset
▲ [WARNING] "UTF-8" will be used instead of unsupported charset "ASCII" [unsupported-@charset] example.css:1:9: 1 │ @charset "ASCII"; ╵ ~~~~~~~
unsupported-@namespace
▲ [WARNING] "@namespace" rules are not supported [unsupported-@namespace] example.css:1:0: 1 │ @namespace "ns"; ╵ ~~~~~~~~~~
unsupported-css-property
▲ [WARNING] "widht" is not a known CSS property [unsupported-css-property] example.css:1:6: 1 │ div { widht: 1px } │ ~~~~~ ╵ width Did you mean "width" instead?
unsupported-css-nesting
▲ [WARNING] Transforming this CSS nesting syntax is not supported in the configured target environment ("chrome50") [unsupported-css-nesting] example.css:2:5: 2 │ .foo & { ╵ ^ The nesting transform for this case must generate an ":is(...)" but the configured target environment does not support the ":is" pseudo-class.
-
捆绑器
ambiguous-reexport
▲ [WARNING] Re-export of "foo" in "example.js" is ambiguous and has been removed [ambiguous-reexport] One definition of "foo" comes from "a.js" here: a.js:1:11: 1 │ export let foo = 1 ╵ ~~~ Another definition of "foo" comes from "b.js" here: b.js:1:11: 1 │ export let foo = 2 ╵ ~~~
different-path-case
▲ [WARNING] Use "foo.js" instead of "Foo.js" to avoid issues with case-sensitive file systems [different-path-case] example.js:2:7: 2 │ import "./Foo.js" ╵ ~~~~~~~~~~
empty-glob
▲ [WARNING] The glob pattern import("./icon-*.json") did not match any files [empty-glob] example.js:2:16: 2 │ return import("./icon-" + name + ".json") ╵ ~~~~~~~~~~~~~~~~~~~~~~~~~~
ignored-bare-import
▲ [WARNING] Ignoring this import because "node_modules/foo/index.js" was marked as having no side effects [ignored-bare-import] example.js:1:7: 1 │ import "foo" ╵ ~~~~~ "sideEffects" is false in the enclosing "package.json" file: node_modules/foo/package.json:2:2: 2 │ "sideEffects": false ╵ ~~~~~~~~~~~~~
ignored-dynamic-import
▲ [WARNING] Importing "foo" was allowed even though it could not be resolved because dynamic import failures appear to be handled here: [ignored-dynamic-import] example.js:1:7: 1 │ import("foo").catch(e => { ╵ ~~~~~ The handler for dynamic import failures is here: example.js:1:14: 1 │ import("foo").catch(e => { ╵ ~~~~~
import-is-undefined
▲ [WARNING] Import "foo" will always be undefined because the file "foo.js" has no exports [import-is-undefined] example.js:1:9: 1 │ import { foo } from "./foo" ╵ ~~~
require-resolve-not-external
▲ [WARNING] "foo" should be marked as external for use with "require.resolve" [require-resolve-not-external] example.js:1:26: 1 │ let foo = require.resolve("foo") ╵ ~~~~~
-
源映射
invalid-source-mappings
▲ [WARNING] Bad "mappings" data in source map at character 3: Invalid original column value: -2 [invalid-source-mappings] example.js.map:2:18: 2 │ "mappings": "aAAFA,UAAU;;" ╵ ^ The source map "example.js.map" was referenced by the file "example.js" here: example.js:1:21: 1 │ //# sourceMappingURL=example.js.map ╵ ~~~~~~~~~~~~~~
sections-in-source-map
▲ [WARNING] Source maps with "sections" are not supported [sections-in-source-map] example.js.map:2:2: 2 │ "sections": [] ╵ ~~~~~~~~~~ The source map "example.js.map" was referenced by the file "example.js" here: example.js:1:21: 1 │ //# sourceMappingURL=example.js.map ╵ ~~~~~~~~~~~~~~
missing-source-map
▲ [WARNING] Cannot read file ".": is a directory [missing-source-map] example.js:1:21: 1 │ //# sourceMappingURL=. ╵ ^
unsupported-source-map-comment
▲ [WARNING] Unsupported source map comment: could not decode percent-escaped data: invalid URL escape "%\"" [unsupported-source-map-comment] example.js:1:21: 1 │ //# sourceMappingURL=data:application/json,"%" ╵ ~~~~~~~~~~~~~~~~~~~~~~~~~
-
解析器
package.json
▲ [WARNING] "esm" is not a valid value for the "type" field [package.json] package.json:1:10: 1 │ { "type": "esm" } ╵ ~~~~~ The "type" field must be set to either "commonjs" or "module".
tsconfig.json
▲ [WARNING] Unrecognized target environment "ES4" [tsconfig.json] tsconfig.json:1:33: 1 │ { "compilerOptions": { "target": "ES4" } } ╵ ~~~~~
这些消息类型应该相当稳定,但将来可能会添加新的消息类型,偶尔也会删除旧的消息类型。如果删除了消息类型,对该消息类型的任何覆盖将被静默忽略。