插件
插件 API 允许你将代码注入构建过程的各个部分。与 API 的其他部分不同,它无法从命令行使用。你必须编写 JavaScript 或 Go 代码才能使用插件 API。插件也只适用于 build API,不适用于 transform API。
#查找插件
如果你正在寻找现有的 esbuild 插件,你应该查看 现有的 esbuild 插件列表。此列表中的插件是由作者故意添加的,旨在供 esbuild 社区中的其他人使用。
如果你想分享你的 esbuild 插件,你应该
- 发布到 npm 以便其他人可以安装它。
- 将其添加到 现有的 esbuild 插件列表 以便其他人可以找到它。
#使用插件
esbuild 插件是一个具有 name
和 setup
函数的对象。它们被传递到 build API 调用的数组中。setup
函数在每次构建 API 调用时运行一次。
这是一个简单的插件示例,它允许你在构建时导入当前的环境变量
import * as esbuild from 'esbuild'
let envPlugin = {
name: 'env',
setup(build) {
// Intercept import paths called "env" so esbuild doesn't attempt
// to map them to a file system location. Tag them with the "env-ns"
// namespace to reserve them for this plugin.
build.onResolve({ filter: /^env$/ }, args => ({
path: args.path,
namespace: 'env-ns',
}))
// Load paths tagged with the "env-ns" namespace and behave as if
// they point to a JSON file containing the environment variables.
build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => ({
contents: JSON.stringify(process.env),
loader: 'json',
}))
},
}
await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
outfile: 'out.js',
plugins: [envPlugin],
})
package main
import "encoding/json"
import "os"
import "strings"
import "github.com/evanw/esbuild/pkg/api"
var envPlugin = api.Plugin{
Name: "env",
Setup: func(build api.PluginBuild) {
// Intercept import paths called "env" so esbuild doesn't attempt
// to map them to a file system location. Tag them with the "env-ns"
// namespace to reserve them for this plugin.
build.OnResolve(api.OnResolveOptions{Filter: `^env$`},
func(args api.OnResolveArgs) (api.OnResolveResult, error) {
return api.OnResolveResult{
Path: args.Path,
Namespace: "env-ns",
}, nil
})
// Load paths tagged with the "env-ns" namespace and behave as if
// they point to a JSON file containing the environment variables.
build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: "env-ns"},
func(args api.OnLoadArgs) (api.OnLoadResult, error) {
mappings := make(map[string]string)
for _, item := range os.Environ() {
if equals := strings.IndexByte(item, '='); equals != -1 {
mappings[item[:equals]] = item[equals+1:]
}
}
bytes, err := json.Marshal(mappings)
if err != nil {
return api.OnLoadResult{}, err
}
contents := string(bytes)
return api.OnLoadResult{
Contents: &contents,
Loader: api.LoaderJSON,
}, nil
})
},
}
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Outfile: "out.js",
Plugins: []api.Plugin{envPlugin},
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
你会像这样使用它
import { PATH } from 'env'
console.log(`PATH is ${PATH}`)
#概念
为 esbuild 编写插件的工作方式与为其他捆绑器编写插件略有不同。在开发你的插件之前,了解以下概念非常重要
#命名空间
每个模块都有一个关联的命名空间。默认情况下,esbuild 在 file
命名空间中运行,它对应于文件系统上的文件。但是 esbuild 也可以处理没有对应文件系统位置的“虚拟”模块。当使用 stdin 提供模块时,就会发生这种情况。
插件可用于创建虚拟模块。虚拟模块通常使用除 file
之外的命名空间来区分它们与文件系统模块。通常,命名空间特定于创建它们的插件。例如,下面的示例 HTTP 插件 对下载的文件使用 http-url
命名空间。
#过滤器
每个回调都必须提供一个正则表达式作为过滤器。esbuild 使用它来跳过在路径不匹配其过滤器时调用回调,这用于性能。从 esbuild 的高度并行内部调用到单线程 JavaScript 代码非常昂贵,应尽可能避免,以实现最大速度。
你应该尝试使用过滤器正则表达式,而不是在可能的情况下使用 JavaScript 代码进行过滤。这更快,因为正则表达式在 esbuild 内部进行评估,而无需调用 JavaScript。例如,下面的示例 HTTP 插件 使用 ^https?://
的过滤器来确保仅对以 http://
或 https://
开头的路径才会产生运行插件的性能开销。
允许的正则表达式语法是 Go 的 正则表达式引擎 支持的语法。这与 JavaScript 略有不同。具体来说,不支持前瞻、后顾和反向引用。Go 的正则表达式引擎旨在避免可能影响 JavaScript 正则表达式的灾难性指数时间最坏情况性能问题。
请注意,命名空间也可以用于过滤。回调必须提供一个过滤器正则表达式,但也可以选择提供一个命名空间来进一步限制匹配的路径。这对于“记住”虚拟模块来自哪里很有用。请记住,命名空间使用精确字符串相等性测试而不是正则表达式进行匹配,因此与模块路径不同,它们不适合存储任意数据。
#On-resolve 回调
使用 onResolve
添加的回调将在 esbuild 构建的每个模块中的每个导入路径上运行。回调可以自定义 esbuild 如何进行路径解析。例如,它可以拦截导入路径并将它们重定向到其他地方。它还可以将路径标记为外部。这是一个例子
import * as esbuild from 'esbuild'
import path from 'node:path'
let exampleOnResolvePlugin = {
name: 'example',
setup(build) {
// Redirect all paths starting with "images/" to "./public/images/"
build.onResolve({ filter: /^images\// }, args => {
return { path: path.join(args.resolveDir, 'public', args.path) }
})
// Mark all paths starting with "http://" or "https://" as external
build.onResolve({ filter: /^https?:\/\// }, args => {
return { path: args.path, external: true }
})
},
}
await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
outfile: 'out.js',
plugins: [exampleOnResolvePlugin],
loader: { '.png': 'binary' },
})
package main
import "os"
import "path/filepath"
import "github.com/evanw/esbuild/pkg/api"
var exampleOnResolvePlugin = api.Plugin{
Name: "example",
Setup: func(build api.PluginBuild) {
// Redirect all paths starting with "images/" to "./public/images/"
build.OnResolve(api.OnResolveOptions{Filter: `^images/`},
func(args api.OnResolveArgs) (api.OnResolveResult, error) {
return api.OnResolveResult{
Path: filepath.Join(args.ResolveDir, "public", args.Path),
}, nil
})
// Mark all paths starting with "http://" or "https://" as external
build.OnResolve(api.OnResolveOptions{Filter: `^https?://`},
func(args api.OnResolveArgs) (api.OnResolveResult, error) {
return api.OnResolveResult{
Path: args.Path,
External: true,
}, nil
})
},
}
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Outfile: "out.js",
Plugins: []api.Plugin{exampleOnResolvePlugin},
Write: true,
Loader: map[string]api.Loader{
".png": api.LoaderBinary,
},
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
回调可以返回而不提供路径,将路径解析的责任传递给下一个回调。对于给定的导入路径,来自所有插件的所有 onResolve
回调将按照它们注册的顺序运行,直到其中一个承担路径解析的责任。如果没有任何回调返回路径,esbuild 将运行其默认路径解析逻辑。
请记住,许多回调可能同时运行。在 JavaScript 中,如果你的回调执行可以在另一个线程上运行的昂贵工作,例如 fs.
,你应该使回调 async
并使用 await
(在这种情况下使用 fs.
)以允许其他代码在同时运行。在 Go 中,每个回调都可能在单独的 goroutine 上运行。如果你的插件使用任何共享数据结构,请确保你已到位适当的同步。
#On-resolve 选项
onResolve
API 旨在在 setup
函数中调用,并注册一个回调,以便在某些情况下触发。它接受一些选项
interface OnResolveOptions {
filter: RegExp;
namespace?: string;
}
type OnResolveOptions struct {
Filter string
Namespace string
}
filter
每个回调都必须提供一个过滤器,它是一个正则表达式。当路径不匹配此过滤器时,注册的回调将被跳过。你可以阅读更多关于过滤器的信息 这里。
namespace
这是可选的。如果提供,回调仅在提供命名空间中的模块内的路径上运行。你可以阅读更多关于命名空间的信息 这里。
#On-resolve 参数
当 esbuild 调用由 onResolve
注册的回调时,它将提供这些参数,其中包含有关导入路径的信息
interface OnResolveArgs {
path: string;
importer: string;
namespace: string;
resolveDir: string;
kind: ResolveKind;
pluginData: any;
}
type ResolveKind =
| 'entry-point'
| 'import-statement'
| 'require-call'
| 'dynamic-import'
| 'require-resolve'
| 'import-rule'
| 'composes-from'
| 'url-token'
type OnResolveArgs struct {
Path string
Importer string
Namespace string
ResolveDir string
Kind ResolveKind
PluginData interface{}
}
const (
ResolveEntryPoint ResolveKind
ResolveJSImportStatement ResolveKind
ResolveJSRequireCall ResolveKind
ResolveJSDynamicImport ResolveKind
ResolveJSRequireResolve ResolveKind
ResolveCSSImportRule ResolveKind
ResolveCSSComposesFrom ResolveKind
ResolveCSSURLToken ResolveKind
)
path
这是来自底层模块源代码的逐字未解析路径。它可以采用任何形式。虽然 esbuild 的默认行为是将导入路径解释为相对路径或包名称,但插件可用于引入新的路径形式。例如,下面的示例 HTTP 插件 对以
http://
开头的路径赋予特殊含义。importer
这是包含此要解析的导入的模块的路径。请注意,此路径仅在命名空间为
file
时才保证是文件系统路径。如果你想相对于包含导入模块的目录解析路径,你应该使用resolveDir
,因为它也适用于虚拟模块。namespace
这是包含此要解析的导入的模块的命名空间,由 on-load 回调 设置,该回调加载了此文件。对于使用 esbuild 默认行为加载的模块,这默认为
file
命名空间。你可以阅读更多关于命名空间的信息 这里。resolveDir
这是在将导入路径解析为文件系统上的真实路径时要使用的文件系统目录。对于
file
命名空间中的模块,此值默认为模块路径的目录部分。对于虚拟模块,此值默认为空,但 on-load 回调 可以选择为虚拟模块也提供解析目录。如果发生这种情况,它将提供给要解析该文件中的未解析路径的解析回调。kind
这表示要解析的路径是如何导入的。例如,
'entry-
表示路径作为入口点路径提供给 API,point' 'import-
表示路径来自 JavaScriptstatement' import
或export
语句,而'import-
表示路径来自 CSSrule' @import
规则。pluginData
此属性从之前的插件传递,由 on-load 回调 设置,该回调加载了此文件。
#On-resolve 结果
这是可以使用 onResolve
添加的回调返回的对象,以提供自定义路径解析。如果你想从回调中返回而不提供路径,只需返回默认值(因此在 JavaScript 中为 undefined
,在 Go 中为 OnResolveResult{}
)。以下是可以返回的可选属性
interface OnResolveResult {
errors?: Message[];
external?: boolean;
namespace?: string;
path?: string;
pluginData?: any;
pluginName?: string;
sideEffects?: boolean;
suffix?: string;
warnings?: Message[];
watchDirs?: string[];
watchFiles?: string[];
}
interface Message {
text: string;
location: Location | null;
detail: any; // The original error from a JavaScript plugin, if applicable
}
interface Location {
file: string;
namespace: string;
line: number; // 1-based
column: number; // 0-based, in bytes
length: number; // in bytes
lineText: string;
}
type OnResolveResult struct {
Errors []Message
External bool
Namespace string
Path string
PluginData interface{}
PluginName string
SideEffects SideEffects
Suffix string
Warnings []Message
WatchDirs []string
WatchFiles []string
}
type Message struct {
Text string
Location *Location
Detail interface{} // The original error from a Go plugin, if applicable
}
type Location struct {
File string
Namespace string
Line int // 1-based
Column int // 0-based, in bytes
Length int // in bytes
LineText string
}
path
path
将其设置为非空字符串以将导入解析为特定路径。如果设置了此项,将不再为此模块中的此导入路径运行更多 on-resolve 回调。如果未设置此项,esbuild 将继续运行在当前回调之后注册的 on-resolve 回调。然后,如果路径仍然未解析,esbuild 将默认解析相对于当前模块的解析目录的路径。
external
namespace
将其设置为
true
以将模块标记为 外部,这意味着它不会包含在捆绑包中,而是在运行时导入。namespace
- 这是与解析路径关联的命名空间。如果留空,它将默认为非外部路径的
file
命名空间。文件命名空间中的路径必须是当前文件系统的绝对路径(因此在 Unix 上以正斜杠开头,在 Windows 上以驱动器号开头)。如果你想解析为非文件系统路径的路径,你应该将命名空间设置为除
file
或空字符串以外的其他内容。这告诉 esbuild 不要将路径视为指向文件系统上的内容。errors
和warnings
- 这些属性允许你将路径解析期间生成的任何日志消息传递给 esbuild,这些消息将在终端中根据当前 日志级别 显示,并最终出现在最终构建结果中。例如,如果你正在调用一个库,并且该库可以返回错误和/或警告,你将希望使用这些属性转发它们。
如果你只有一个错误要返回,你不必通过
errors
传递它。你只需在 JavaScript 中抛出错误,或者在 Go 中将error
对象作为第二个返回值返回。如果 `watchFiles` 数组中的任何文件自上次构建以来发生更改,则将触发重建。更改检测比较复杂,可能会检查文件内容和/或文件的元数据。
如果 `watchDirs` 数组中任何目录的目录条目列表自上次构建以来发生更改,也将触发重建。请注意,这不会检查这些目录中任何文件的任何内容,也不会检查任何子目录。可以将其视为检查 Unix `ls` 命令的输出。
为了稳健性,您应该包含在评估插件期间使用过的所有文件系统路径。例如,如果您的插件执行类似于 `require.resolve()` 的操作,则需要包含所有“此文件是否存在”检查的路径,而不仅仅是最终路径。否则,可能会创建导致构建过时的新文件,但 esbuild 无法检测到它,因为该路径未列出。
pluginName
此属性允许您将此插件的名称替换为此路径解析操作的另一个名称。它对于通过此插件代理另一个插件很有用。例如,它允许您拥有一个将转发到包含多个插件的子进程的单个插件。您可能不需要使用它。
pluginData
此属性将传递给插件链中运行的下一个插件。如果您从 `onLoad` 插件中返回它,它将传递给该文件中任何导入的 `onResolve` 插件,如果您从 `onResolve` 插件中返回它,当它加载文件时,将传递一个任意的插件到 `onLoad` 插件(它是任意的,因为关系是多对一)。这对于在不同的插件之间传递数据很有用,而无需它们直接协调。
sideEffects
将此属性设置为 false 会告诉 esbuild,如果导入的名称未使用,则可以删除此模块的导入。这就像在相应的 `package.json` 文件中指定了 `"sideEffects": false` 一样。例如,如果 `x` 未使用且 `y` 已标记为 `sideEffects: false`,则 `import { x }
from "y"` 可能会被完全删除。您可以在 Webpack 关于该功能的文档 中阅读有关 `sideEffects` 含义的更多信息。 suffix
在此处返回一个值可以让您传递一个可选的 URL 查询或哈希以附加到路径中,该路径不包含在路径本身中。在路径由 esbuild 本身或其他插件不知情的某些内容处理的情况下,单独存储此信息是有益的。
例如,一个 on-resolve 插件可能会为构建中的 `eot` 文件返回一个 `?#iefix` 后缀,该构建对以 `eot` 结尾的路径使用不同的 on-load 插件。将后缀分开意味着后缀仍然与路径相关联,但 `eot` 插件仍然可以匹配文件,而无需了解任何有关后缀的信息。
如果您确实设置了后缀,它必须以 `?` 或 `#` 开头,因为它旨在作为 URL 查询或哈希。此功能具有一些模糊的用途,例如解决 IE8 的 CSS 解析器中的错误,否则可能不太有用。如果您确实使用它,请记住,esbuild 将每个唯一的命名空间、路径和后缀组合视为一个唯一的模块标识符,因此通过为相同的路径返回不同的后缀,您告诉 esbuild 创建模块的另一个副本。
#On-load 回调
使用 `onLoad` 添加的回调将针对每个尚未标记为外部的唯一路径/命名空间对运行。它的作用是返回模块的内容并告诉 esbuild 如何解释它。以下是一个将 `txt` 文件转换为单词数组的示例插件
import * as esbuild from 'esbuild'
import fs from 'node:fs'
let exampleOnLoadPlugin = {
name: 'example',
setup(build) {
// Load ".txt" files and return an array of words
build.onLoad({ filter: /\.txt$/ }, async (args) => {
let text = await fs.promises.readFile(args.path, 'utf8')
return {
contents: JSON.stringify(text.split(/\s+/)),
loader: 'json',
}
})
},
}
await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
outfile: 'out.js',
plugins: [exampleOnLoadPlugin],
})
package main
import "encoding/json"
import "io/ioutil"
import "os"
import "strings"
import "github.com/evanw/esbuild/pkg/api"
var exampleOnLoadPlugin = api.Plugin{
Name: "example",
Setup: func(build api.PluginBuild) {
// Load ".txt" files and return an array of words
build.OnLoad(api.OnLoadOptions{Filter: `\.txt$`},
func(args api.OnLoadArgs) (api.OnLoadResult, error) {
text, err := ioutil.ReadFile(args.Path)
if err != nil {
return api.OnLoadResult{}, err
}
bytes, err := json.Marshal(strings.Fields(string(text)))
if err != nil {
return api.OnLoadResult{}, err
}
contents := string(bytes)
return api.OnLoadResult{
Contents: &contents,
Loader: api.LoaderJSON,
}, nil
})
},
}
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Outfile: "out.js",
Plugins: []api.Plugin{exampleOnLoadPlugin},
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
回调可以在不提供模块内容的情况下返回。在这种情况下,加载模块的责任将传递给下一个注册的回调。对于给定的模块,来自所有插件的所有 `onLoad` 回调将按照它们注册的顺序运行,直到一个回调承担加载模块的责任。如果没有任何回调为模块返回内容,esbuild 将运行其默认的模块加载逻辑。
请记住,许多回调可能同时运行。在 JavaScript 中,如果您的回调执行可以在另一个线程上运行的昂贵工作,例如 `fs.
#On-load 选项
`onLoad` API 旨在在 `setup` 函数中调用,并注册一个回调以在某些情况下触发。它需要一些选项
interface OnLoadOptions {
filter: RegExp;
namespace?: string;
}
type OnLoadOptions struct {
Filter string
Namespace string
}
filter
每个回调都必须提供一个过滤器,它是一个正则表达式。当路径不匹配此过滤器时,注册的回调将被跳过。你可以阅读更多关于过滤器的信息 这里。
namespace
这是可选的。如果提供,回调仅在提供命名空间中的模块内的路径上运行。你可以阅读更多关于命名空间的信息 这里。
#On-load 参数
当 esbuild 调用由 `onLoad` 注册的回调时,它将提供这些参数,其中包含有关要加载的模块的信息
interface OnLoadArgs {
path: string;
namespace: string;
suffix: string;
pluginData: any;
with: Record<string, string>;
}
type OnLoadArgs struct {
Path string
Namespace string
Suffix string
PluginData interface{}
With map[string]string
}
path
这是模块的完全解析路径。如果命名空间为 `file`,则应将其视为文件系统路径,否则路径可以采用任何形式。例如,下面的示例 HTTP 插件 对以 `http://` 开头的路径赋予了特殊含义。
namespace
这是模块路径所在的命名空间,由 on-resolve 回调 设置,该回调解析了此文件。对于使用 esbuild 的默认行为加载的模块,它默认为 `file` 命名空间。您可以在 此处 阅读有关命名空间的更多信息。
suffix
这是文件路径末尾的 URL 查询和/或哈希(如果有)。它要么由 esbuild 的本机路径解析行为填充,要么由 on-resolve 回调 返回,该回调解析了此文件。它与路径分开存储,以便大多数插件只需处理路径并忽略后缀。esbuild 中内置的 on-load 行为只是忽略后缀并仅从其路径加载文件。
作为背景,IE8 的 CSS 解析器有一个错误,它认为某些 URL 扩展到最后一个 `)` 而不是第一个 `)`。因此,CSS 代码 `url('Foo.eot')
format('eot')` 被错误地认为具有 `Foo.eot') format('eot` 的 URL。为了避免这种情况,人们通常会添加类似 `?#iefix` 的内容,以便 IE8 将 URL 视为 `Foo.eot?#iefix') format('eot`。然后 URL 的路径部分为 `Foo.eot`,查询部分为 `?#iefix') format('eot`,这意味着 IE8 可以通过丢弃查询来找到文件 `Foo.eot`。 添加后缀功能是为了处理包含这些 hack 的 CSS 文件。如果所有匹配 `*.eot` 的文件都被标记为外部,则 `Foo.eot?#iefix` 的 URL 应被视为 外部,但 `?#iefix` 后缀应仍然存在于最终输出文件中。
pluginData
此属性从上一个插件传递,由 on-resolve 回调 设置,该回调在插件链中运行。
with
它包含一个映射,其中包含在用于导入此模块的导入语句中存在的 导入属性。例如,使用 `with {
type: 'json' }` 导入的模块将为插件提供 `with` 值 `{ type: 'json' }`。对于每个导入属性的唯一组合,都会单独加载给定模块,因此这些属性保证已由用于导入此模块的所有导入语句提供。这意味着插件可以使用它们来更改此模块的内容。
#On-load 结果
这是可以使用 `onLoad` 添加的回调返回的对象,以提供模块的内容。如果您想从回调中返回而不提供任何内容,只需返回默认值(因此在 JavaScript 中为 `undefined`,在 Go 中为 `OnLoadResult{}`)。以下是可以返回的可选属性
interface OnLoadResult {
contents?: string | Uint8Array;
errors?: Message[];
loader?: Loader;
pluginData?: any;
pluginName?: string;
resolveDir?: string;
warnings?: Message[];
watchDirs?: string[];
watchFiles?: string[];
}
interface Message {
text: string;
location: Location | null;
detail: any; // The original error from a JavaScript plugin, if applicable
}
interface Location {
file: string;
namespace: string;
line: number; // 1-based
column: number; // 0-based, in bytes
length: number; // in bytes
lineText: string;
}
type OnLoadResult struct {
Contents *string
Errors []Message
Loader Loader
PluginData interface{}
PluginName string
ResolveDir string
Warnings []Message
WatchDirs []string
WatchFiles []string
}
type Message struct {
Text string
Location *Location
Detail interface{} // The original error from a Go plugin, if applicable
}
type Location struct {
File string
Namespace string
Line int // 1-based
Column int // 0-based, in bytes
Length int // in bytes
LineText string
}
contents
将其设置为字符串以指定模块的内容。如果设置了此项,则不会为该解析路径运行更多 on-load 回调。如果未设置此项,esbuild 将继续运行在当前回调之后注册的 on-load 回调。然后,如果内容仍然未设置,esbuild 将默认从文件系统加载内容(如果解析路径位于 `file` 命名空间中)。
loader
这告诉 esbuild 如何解释内容。例如,
js
加载器将内容解释为 JavaScript,而css
加载器将内容解释为 CSS。如果未指定加载器,则默认加载器为 `js`。有关所有内置加载器的完整列表,请参见 内容类型 页面。resolveDir
这是在将此模块中的导入路径解析为文件系统上的真实路径时要使用的文件系统目录。对于 `file` 命名空间中的模块,此值默认为模块路径的目录部分。否则,此值默认为空,除非插件提供一个。如果插件没有提供一个,esbuild 的默认行为将不会解析此模块中的任何导入。此目录将传递给在该模块中未解析的导入路径上运行的任何 on-resolve 回调。
- 这是与解析路径关联的命名空间。如果留空,它将默认为非外部路径的
file
命名空间。文件命名空间中的路径必须是当前文件系统的绝对路径(因此在 Unix 上以正斜杠开头,在 Windows 上以驱动器号开头)。如果你想解析为非文件系统路径的路径,你应该将命名空间设置为除
file
或空字符串以外的其他内容。这告诉 esbuild 不要将路径视为指向文件系统上的内容。errors
和warnings
- 这些属性允许你将路径解析期间生成的任何日志消息传递给 esbuild,这些消息将在终端中根据当前 日志级别 显示,并最终出现在最终构建结果中。例如,如果你正在调用一个库,并且该库可以返回错误和/或警告,你将希望使用这些属性转发它们。
如果你只有一个错误要返回,你不必通过
errors
传递它。你只需在 JavaScript 中抛出错误,或者在 Go 中将error
对象作为第二个返回值返回。如果 `watchFiles` 数组中的任何文件自上次构建以来发生更改,则将触发重建。更改检测比较复杂,可能会检查文件内容和/或文件的元数据。
如果 `watchDirs` 数组中任何目录的目录条目列表自上次构建以来发生更改,也将触发重建。请注意,这不会检查这些目录中任何文件的任何内容,也不会检查任何子目录。可以将其视为检查 Unix `ls` 命令的输出。
为了稳健性,您应该包含在评估插件期间使用过的所有文件系统路径。例如,如果您的插件执行类似于 `require.resolve()` 的操作,则需要包含所有“此文件是否存在”检查的路径,而不仅仅是最终路径。否则,可能会创建导致构建过时的新文件,但 esbuild 无法检测到它,因为该路径未列出。
pluginName
此属性允许您将此插件的名称替换为此模块加载操作的另一个名称。它对于通过此插件代理另一个插件很有用。例如,它允许您拥有一个将转发到包含多个插件的子进程的单个插件。您可能不需要使用它。
pluginData
此属性将传递给插件链中运行的下一个插件。如果您从 `onLoad` 插件中返回它,它将传递给该文件中任何导入的 `onResolve` 插件,如果您从 `onResolve` 插件中返回它,当它加载文件时,将传递一个任意的插件到 `onLoad` 插件(它是任意的,因为关系是多对一)。这对于在不同的插件之间传递数据很有用,而无需它们直接协调。
#缓存您的插件
由于 esbuild 非常快,因此插件评估通常是使用 esbuild 构建时的主要瓶颈。插件评估的缓存留给每个插件,而不是作为 esbuild 本身的一部分,因为缓存失效是特定于插件的。如果您正在编写一个需要缓存才能快速运行的慢速插件,则必须自己编写缓存逻辑。
缓存本质上是一个映射,它记忆表示您的插件的转换函数。映射的键通常包含转换函数的输入,映射的值通常包含转换函数的输出。此外,映射通常具有一些形式的最近最少使用缓存逐出策略,以避免随着时间的推移不断增长。
缓存可以存储在内存中(有利于与 esbuild 的 重建 API 一起使用)、磁盘上(有利于跨不同的构建脚本调用进行缓存),甚至存储在服务器上(有利于可以跨不同的开发人员机器共享的非常慢的转换)。将缓存存储在何处是特定于情况的,取决于您的插件。
这是一个简单的缓存示例。假设我们要缓存函数 `slowTransform()`,该函数以 `*.example` 格式的文件内容作为输入,并将其转换为 JavaScript。一个避免在使用 esbuild 的 重建 API 时重复调用此函数的内存中缓存可能看起来像这样
import fs from 'node:fs'
let examplePlugin = {
name: 'example',
setup(build) {
let cache = new Map
build.onLoad({ filter: /\.example$/ }, async (args) => {
let input = await fs.promises.readFile(args.path, 'utf8')
let key = args.path
let value = cache.get(key)
if (!value || value.input !== input) {
let contents = slowTransform(input)
value = { input, output: { contents } }
cache.set(key, value)
}
return value.output
})
}
}
关于上面的缓存代码的一些重要注意事项
上面的代码中没有缓存逐出策略。如果向缓存映射添加了越来越多的键,内存使用量将继续增长。
缓存失效只有在
slowTransform()
是一个 纯函数(意味着函数的输出 *仅* 依赖于函数的输入)并且函数的所有输入都以某种方式被捕获到缓存映射的查找中时才有效。例如,如果转换函数自动读取其他文件的内容,并且输出也依赖于这些文件的内容,那么当这些文件发生更改时,缓存将无法失效,因为它们没有包含在缓存键中。这部分很容易搞错,所以值得仔细研究一个具体的例子。考虑一个实现编译为 CSS 语言的插件。如果该插件通过解析导入文件并将其捆绑或使任何导出的变量声明可用于导入代码来实现
@import
规则,那么如果它只检查导入文件的內容是否发生更改,你的插件将不正确,因为导入文件的更改也可能使缓存失效。你可能认为你可以将导入文件的内容添加到缓存键中来解决这个问题。然而,即使这样也可能不正确。例如,假设这个插件使用
require.resolve()
来将导入路径解析为绝对文件路径。这是一种常见的方法,因为它使用 node 的内置路径解析,可以解析到包内的路径。此函数通常在返回解析后的路径之前对不同位置的文件进行多次检查。例如,从文件src/entry.css
导入路径pkg/file
可能会检查以下位置(是的,node 的包解析算法非常低效)src/node_modules/pkg/file src/node_modules/pkg/file.css src/node_modules/pkg/file/package.json src/node_modules/pkg/file/main src/node_modules/pkg/file/main.css src/node_modules/pkg/file/main/index.css src/node_modules/pkg/file/index.css node_modules/pkg/file node_modules/pkg/file.css node_modules/pkg/file/package.json node_modules/pkg/file/main node_modules/pkg/file/main.css node_modules/pkg/file/main/index.css node_modules/pkg/file/index.css
假设导入
pkg/file
最终解析为绝对路径node_modules/
。即使你缓存了导入文件和导入文件的内容,并验证了两个文件的内容是否仍然相同,然后再重用缓存条目,如果pkg/ file/ index.css require.resolve()
检查的其他文件之一自缓存条目添加以来已被创建或删除,缓存条目仍然可能过时。正确缓存这一点本质上涉及始终重新运行所有此类路径解析,即使没有任何输入文件发生更改,并验证所有路径解析都没有发生更改。这些缓存键仅适用于内存缓存。使用相同的缓存键实现文件系统缓存将是不正确的。虽然内存缓存保证每次构建都运行相同的代码,因为代码也存储在内存中,但文件系统缓存可能被两个包含不同代码的独立构建访问。具体来说,
slowTransform()
函数的代码可能在两次构建之间发生了更改。这可能发生在各种情况下。包含函数
slowTransform()
的包可能已更新,或者其传递依赖项之一可能已更新,即使你已固定了包的版本,因为 npm 如何处理语义版本控制,或者有人可能在文件系统上 修改了包内容,或者转换函数可能正在调用 node API,并且不同的构建可能在不同的 node 版本上运行。如果你想将缓存存储在文件系统上,你应该通过在缓存键中存储转换函数代码的某种表示来防止对转换函数代码的更改。这通常是某种形式的 哈希,其中包含所有相关包中所有相关文件的内容,以及其他细节,例如你当前运行的 node 版本。使所有这些都正确并非易事。
#启动时回调
注册一个启动时回调,以便在新的构建开始时收到通知。这会触发所有构建,而不仅仅是初始构建,因此它对于 重新构建、监视模式 和 服务模式 特别有用。以下是添加启动时回调的方法
let examplePlugin = {
name: 'example',
setup(build) {
build.onStart(() => {
console.log('build started')
})
},
}
package main
import "fmt"
import "github.com/evanw/esbuild/pkg/api"
import "os"
var examplePlugin = api.Plugin{
Name: "example",
Setup: func(build api.PluginBuild) {
build.OnStart(func() (api.OnStartResult, error) {
fmt.Fprintf(os.Stderr, "build started\n")
return api.OnStartResult{}, nil
})
},
}
func main() {
}
你不应该将启动时回调用于初始化,因为它可能被运行多次。如果你想初始化某些东西,只需将你的插件初始化代码直接放在 setup
函数中即可。
启动时回调可以是 async
并且可以返回一个 Promise。来自所有插件的所有启动时回调都会并发运行,然后构建会等待所有启动时回调完成,然后再继续。启动时回调可以选择返回错误和/或警告,以包含在构建中。
请注意,启动时回调无法修改 构建选项。初始构建选项只能在 setup
函数中修改,并且在 setup
返回后被使用一次。第一个构建之后的每次构建都会重用相同的初始选项,因此初始选项永远不会被重新使用,并且在启动回调中对 build.initialOptions
的修改会被忽略。
#结束时回调
注册一个结束时回调,以便在新的构建结束时收到通知。这会触发所有构建,而不仅仅是初始构建,因此它对于 重新构建、监视模式 和 服务模式 特别有用。以下是添加结束时回调的方法
let examplePlugin = {
name: 'example',
setup(build) {
build.onEnd(result => {
console.log(`build ended with ${result.errors.length} errors`)
})
},
}
package main
import "fmt"
import "github.com/evanw/esbuild/pkg/api"
import "os"
var examplePlugin = api.Plugin{
Name: "example",
Setup: func(build api.PluginBuild) {
build.OnEnd(func(result *api.BuildResult) (api.OnEndResult, error) {
fmt.Fprintf(os.Stderr, "build ended with %d errors\n", len(result.Errors))
return api.OnEndResult{}, nil
})
},
}
func main() {
}
所有结束时回调都会按顺序运行,每个回调都可以访问最终的构建结果。它可以在返回之前修改构建结果,并且可以通过返回一个 Promise 来延迟构建的结束。如果你想能够检查构建图,你应该在 初始选项 上启用 元文件 设置,构建图将作为构建结果对象上的 metafile
属性返回。
#销毁时回调
注册一个销毁时回调,以便在不再使用插件时执行清理。它将在每次 build()
调用后被调用,无论构建是否失败,以及在给定构建上下文上的第一次 dispose()
调用后。以下是添加销毁时回调的方法
let examplePlugin = {
name: 'example',
setup(build) {
build.onDispose(() => {
console.log('This plugin is no longer used')
})
},
}
package main
import "fmt"
import "github.com/evanw/esbuild/pkg/api"
var examplePlugin = api.Plugin{
Name: "example",
Setup: func(build api.PluginBuild) {
build.OnDispose(func() {
fmt.Println("This plugin is no longer used")
})
},
}
func main() {
}
#访问构建选项
插件可以在 setup
方法中访问初始构建选项。这使你能够检查构建的配置方式,以及在构建开始之前修改构建选项。以下是一个示例
let examplePlugin = {
name: 'auto-node-env',
setup(build) {
const options = build.initialOptions
options.define = options.define || {}
options.define['process.env.NODE_ENV'] =
options.minify ? '"production"' : '"development"'
},
}
package main
import "github.com/evanw/esbuild/pkg/api"
var examplePlugin = api.Plugin{
Name: "auto-node-env",
Setup: func(build api.PluginBuild) {
options := build.InitialOptions
if options.Define == nil {
options.Define = map[string]string{}
}
if options.MinifyWhitespace && options.MinifyIdentifiers && options.MinifySyntax {
options.Define[`process.env.NODE_ENV`] = `"production"`
} else {
options.Define[`process.env.NODE_ENV`] = `"development"`
}
},
}
func main() {
}
请注意,在构建开始后对构建选项的修改不会影响构建。特别是,重新构建、监视模式 和 服务模式 不会更新它们的构建选项,如果插件在第一个构建开始后修改了构建选项对象。
#解析路径
当插件从 on-resolve 回调 返回结果时,结果将完全替换 esbuild 的内置路径解析。这使插件可以完全控制路径解析的工作方式,但这意味着如果插件想要具有类似的行为,它可能必须重新实现 esbuild 已经内置的一些行为。例如,插件可能希望在用户的 node_modules
目录中搜索包,这是 esbuild 已经实现的功能。
插件可以选择手动运行 esbuild 的路径解析并检查结果,而不是重新实现 esbuild 的内置行为。这使你能够调整 esbuild 的路径解析的输入和/或输出。以下是一个示例
import * as esbuild from 'esbuild'
let examplePlugin = {
name: 'example',
setup(build) {
build.onResolve({ filter: /^example$/ }, async () => {
const result = await build.resolve('./foo', {
kind: 'import-statement',
resolveDir: './bar',
})
if (result.errors.length > 0) {
return { errors: result.errors }
}
return { path: result.path, external: true }
})
},
}
await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
outfile: 'out.js',
plugins: [examplePlugin],
})
package main
import "os"
import "github.com/evanw/esbuild/pkg/api"
var examplePlugin = api.Plugin{
Name: "example",
Setup: func(build api.PluginBuild) {
build.OnResolve(api.OnResolveOptions{Filter: `^example$`},
func(api.OnResolveArgs) (api.OnResolveResult, error) {
result := build.Resolve("./foo", api.ResolveOptions{
Kind: api.ResolveJSImportStatement,
ResolveDir: "./bar",
})
if len(result.Errors) > 0 {
return api.OnResolveResult{Errors: result.Errors}, nil
}
return api.OnResolveResult{Path: result.Path, External: true}, nil
})
},
}
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Outfile: "out.js",
Plugins: []api.Plugin{examplePlugin},
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
此插件拦截对路径 example
的导入,告诉 esbuild 在目录 ./bar
中解析导入 ./foo
,强制 esbuild 返回的任何路径都被视为外部路径,并将 example
的导入映射到该外部路径。
以下是一些关于此 API 的其他注意事项
如果你没有传递可选的
resolveDir
参数,esbuild 仍然会运行onResolve
插件回调,但不会尝试任何路径解析。esbuild 的所有路径解析逻辑都依赖于resolveDir
参数,包括在node_modules
目录中查找包(因为它需要知道这些node_modules
目录可能在哪里)。如果你想在特定目录中解析文件名,请确保输入路径以
./
开头。否则,输入路径将被视为包路径而不是相对路径。此行为与 esbuild 的正常路径解析逻辑相同。如果路径解析失败,返回的对象上的
errors
属性将是一个非空数组,其中包含错误信息。此函数并不总是会在失败时抛出错误。你需要在调用它之后检查错误。此函数的行为取决于构建配置。这就是为什么它是
build
对象的属性,而不是顶级 API 调用。这也意味着你不能在所有插件setup
函数完成之前调用它,因为这些函数使插件有机会在构建开始时调整构建配置,然后构建配置就会被冻结。因此,resolve
函数在你的onResolve
和/或onLoad
回调中将最有用。目前没有尝试检测无限路径解析循环。在
onResolve
中使用相同的参数调用resolve
几乎肯定是一个坏主意。
#解析选项
resolve
函数将要解析的路径作为第一个参数,并将一个包含可选属性的对象作为第二个参数。此选项对象与 传递给 onResolve
的参数 非常相似。以下是可用的选项
interface ResolveOptions {
kind: ResolveKind;
importer?: string;
namespace?: string;
resolveDir?: string;
pluginData?: any;
}
type ResolveKind =
| 'entry-point'
| 'import-statement'
| 'require-call'
| 'dynamic-import'
| 'require-resolve'
| 'import-rule'
| 'url-token'
type ResolveOptions struct {
Kind ResolveKind
Importer string
Namespace string
ResolveDir string
PluginData interface{}
}
const (
ResolveEntryPoint ResolveKind
ResolveJSImportStatement ResolveKind
ResolveJSRequireCall ResolveKind
ResolveJSDynamicImport ResolveKind
ResolveJSRequireResolve ResolveKind
ResolveCSSImportRule ResolveKind
ResolveCSSURLToken ResolveKind
)
kind
这告诉 esbuild 路径是如何导入的,这会影响路径解析。例如,node 的路径解析规则 规定,使用
'require-call'
导入的路径应该尊重package.json
中"require"
部分中的 条件包导入,而使用'import-statement'
导入的路径应该尊重"import"
部分中的条件包导入。importer
如果设置,这将被解释为包含要解析的导入的模块的路径。这会影响具有
onResolve
回调的插件,这些回调会检查importer
值。namespace
如果设置,这将被解释为包含要解析的导入的模块的命名空间。这会影响具有
onResolve
回调的插件,这些回调会检查namespace
值。你可以 在这里 阅读有关命名空间的更多信息。resolveDir
这是在将导入路径解析为文件系统上的真实路径时要使用的文件系统目录。这必须设置,以便 esbuild 的内置路径解析能够找到给定文件,即使对于非相对包路径也是如此(因为 esbuild 需要知道
node_modules
目录在哪里)。pluginData
此属性可用于将自定义数据传递给与该导入路径匹配的任何 on-resolve 回调。此数据的含义完全由你决定。
#解析结果
resolve
函数返回一个对象,该对象与插件可以 从 onResolve
回调返回的对象 非常相似。它具有以下属性
export interface ResolveResult {
errors: Message[];
external: boolean;
namespace: string;
path: string;
pluginData: any;
sideEffects: boolean;
suffix: string;
warnings: Message[];
}
interface Message {
text: string;
location: Location | null;
detail: any; // The original error from a JavaScript plugin, if applicable
}
interface Location {
file: string;
namespace: string;
line: number; // 1-based
column: number; // 0-based, in bytes
length: number; // in bytes
lineText: string;
}
type ResolveResult struct {
Errors []Message
External bool
Namespace string
Path string
PluginData interface{}
SideEffects bool
Suffix string
Warnings []Message
}
type Message struct {
Text string
Location *Location
Detail interface{} // The original error from a Go plugin, if applicable
}
type Location struct {
File string
Namespace string
Line int // 1-based
Column int // 0-based, in bytes
Length int // in bytes
LineText string
}
path
这是路径解析的结果,如果路径解析失败,则为空字符串。
将其设置为非空字符串以将导入解析为特定路径。如果设置了此项,将不再为此模块中的此导入路径运行更多 on-resolve 回调。如果未设置此项,esbuild 将继续运行在当前回调之后注册的 on-resolve 回调。然后,如果路径仍然未解析,esbuild 将默认解析相对于当前模块的解析目录的路径。
如果路径被标记为 外部,则此值为
true
,这意味着它不会包含在捆绑包中,而是将在运行时导入。namespace
这是与解析后的路径关联的命名空间。你可以 在这里 阅读有关命名空间的更多信息。
- 这是与解析路径关联的命名空间。如果留空,它将默认为非外部路径的
file
命名空间。文件命名空间中的路径必须是当前文件系统的绝对路径(因此在 Unix 上以正斜杠开头,在 Windows 上以驱动器号开头)。这些属性保存路径解析期间生成的任何日志消息,无论是响应此路径解析操作的任何插件还是 esbuild 本身生成的日志消息。这些日志消息不会自动包含在日志中,因此如果你丢弃它们,它们将完全不可见。如果你想将它们包含在日志中,你需要从
onResolve
或onLoad
中返回它们。 pluginData
如果插件响应了此路径解析操作,并从其
onResolve
回调中返回了pluginData
,那么该数据将最终出现在这里。这对于在不同的插件之间传递数据很有用,而无需它们直接协调。sideEffects
除非模块以某种方式被标注为没有副作用,否则此属性将为
true
,在这种情况下,它将为false
。对于在相应的package.json
文件中具有"sideEffects": false
的包,以及如果插件响应此路径解析操作并返回sideEffects: false
,则此属性将为false
。您可以在Webpack关于该功能的文档中了解更多关于sideEffects
的含义。suffix
如果在要解析的路径末尾存在可选的 URL 查询或哈希,并且如果删除它对于路径成功解析是必需的,则它可以包含可选的 URL 查询或哈希。
#示例插件
以下示例插件旨在让您了解使用插件 API 可以完成的不同类型的事情。
#HTTP 插件
此示例演示:使用除文件系统路径之外的路径格式、特定于命名空间的路径解析、将解析和加载回调一起使用。
此插件允许您将 HTTP URL 导入 JavaScript 代码。代码将在构建时自动下载。它支持以下工作流程
import { zip } from 'https://unpkg.com/lodash-es@4.17.15/lodash.js'
console.log(zip([1, 2], ['a', 'b']))
可以使用以下插件来实现。请注意,对于实际使用,下载应该被缓存,但为了简洁起见,本示例中省略了缓存
import * as esbuild from 'esbuild'
import https from 'node:https'
import http from 'node:http'
let httpPlugin = {
name: 'http',
setup(build) {
// Intercept import paths starting with "http:" and "https:" so
// esbuild doesn't attempt to map them to a file system location.
// Tag them with the "http-url" namespace to associate them with
// this plugin.
build.onResolve({ filter: /^https?:\/\// }, args => ({
path: args.path,
namespace: 'http-url',
}))
// We also want to intercept all import paths inside downloaded
// files and resolve them against the original URL. All of these
// files will be in the "http-url" namespace. Make sure to keep
// the newly resolved URL in the "http-url" namespace so imports
// inside it will also be resolved as URLs recursively.
build.onResolve({ filter: /.*/, namespace: 'http-url' }, args => ({
path: new URL(args.path, args.importer).toString(),
namespace: 'http-url',
}))
// When a URL is loaded, we want to actually download the content
// from the internet. This has just enough logic to be able to
// handle the example import from unpkg.com but in reality this
// would probably need to be more complex.
build.onLoad({ filter: /.*/, namespace: 'http-url' }, async (args) => {
let contents = await new Promise((resolve, reject) => {
function fetch(url) {
console.log(`Downloading: ${url}`)
let lib = url.startsWith('https') ? https : http
let req = lib.get(url, res => {
if ([301, 302, 307].includes(res.statusCode)) {
fetch(new URL(res.headers.location, url).toString())
req.abort()
} else if (res.statusCode === 200) {
let chunks = []
res.on('data', chunk => chunks.push(chunk))
res.on('end', () => resolve(Buffer.concat(chunks)))
} else {
reject(new Error(`GET ${url} failed: status ${res.statusCode}`))
}
}).on('error', reject)
}
fetch(args.path)
})
return { contents }
})
},
}
await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
outfile: 'out.js',
plugins: [httpPlugin],
})
package main
import "io/ioutil"
import "net/http"
import "net/url"
import "os"
import "github.com/evanw/esbuild/pkg/api"
var httpPlugin = api.Plugin{
Name: "http",
Setup: func(build api.PluginBuild) {
// Intercept import paths starting with "http:" and "https:" so
// esbuild doesn't attempt to map them to a file system location.
// Tag them with the "http-url" namespace to associate them with
// this plugin.
build.OnResolve(api.OnResolveOptions{Filter: `^https?://`},
func(args api.OnResolveArgs) (api.OnResolveResult, error) {
return api.OnResolveResult{
Path: args.Path,
Namespace: "http-url",
}, nil
})
// We also want to intercept all import paths inside downloaded
// files and resolve them against the original URL. All of these
// files will be in the "http-url" namespace. Make sure to keep
// the newly resolved URL in the "http-url" namespace so imports
// inside it will also be resolved as URLs recursively.
build.OnResolve(api.OnResolveOptions{Filter: ".*", Namespace: "http-url"},
func(args api.OnResolveArgs) (api.OnResolveResult, error) {
base, err := url.Parse(args.Importer)
if err != nil {
return api.OnResolveResult{}, err
}
relative, err := url.Parse(args.Path)
if err != nil {
return api.OnResolveResult{}, err
}
return api.OnResolveResult{
Path: base.ResolveReference(relative).String(),
Namespace: "http-url",
}, nil
})
// When a URL is loaded, we want to actually download the content
// from the internet. This has just enough logic to be able to
// handle the example import from unpkg.com but in reality this
// would probably need to be more complex.
build.OnLoad(api.OnLoadOptions{Filter: ".*", Namespace: "http-url"},
func(args api.OnLoadArgs) (api.OnLoadResult, error) {
res, err := http.Get(args.Path)
if err != nil {
return api.OnLoadResult{}, err
}
defer res.Body.Close()
bytes, err := ioutil.ReadAll(res.Body)
if err != nil {
return api.OnLoadResult{}, err
}
contents := string(bytes)
return api.OnLoadResult{Contents: &contents}, nil
})
},
}
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Outfile: "out.js",
Plugins: []api.Plugin{httpPlugin},
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
该插件首先使用解析器将http://
和https://
URL 移动到http-url
命名空间。设置命名空间告诉 esbuild 不要将这些路径视为文件系统路径。然后,http-url
命名空间的加载器下载模块并将内容返回给 esbuild。从那里,http-url
命名空间中模块内部导入路径的另一个解析器会获取相对路径,并通过相对于导入模块的 URL 解析它们来将它们转换为完整 URL。然后,它反馈到加载器中,允许下载的模块递归地下载其他模块。
#WebAssembly 插件
此示例演示:使用二进制数据、使用导入语句创建虚拟模块、使用相同路径的不同命名空间。
此插件允许您将.wasm
文件导入 JavaScript 代码。它不会生成 WebAssembly 文件本身;这可以通过另一个工具完成,也可以通过修改此示例插件来满足您的需求。它支持以下工作流程
import load from './example.wasm'
load(imports).then(exports => { ... })
当您导入.wasm
文件时,此插件在wasm-stub
命名空间中生成一个虚拟 JavaScript 模块,其中包含一个函数,该函数加载作为默认导出导出的 WebAssembly 模块。该存根模块看起来像这样
import wasm from '/path/to/example.wasm'
export default (imports) =>
WebAssembly.instantiate(wasm, imports).then(
result => result.instance.exports)
然后,该存根模块使用 esbuild 内置的二进制加载器将 WebAssembly 文件本身作为wasm-binary
命名空间中的另一个模块导入。这意味着导入.wasm
文件实际上会生成两个虚拟模块。以下是插件的代码
import * as esbuild from 'esbuild'
import path from 'node:path'
import fs from 'node:fs'
let wasmPlugin = {
name: 'wasm',
setup(build) {
// Resolve ".wasm" files to a path with a namespace
build.onResolve({ filter: /\.wasm$/ }, args => {
// If this is the import inside the stub module, import the
// binary itself. Put the path in the "wasm-binary" namespace
// to tell our binary load callback to load the binary file.
if (args.namespace === 'wasm-stub') {
return {
path: args.path,
namespace: 'wasm-binary',
}
}
// Otherwise, generate the JavaScript stub module for this
// ".wasm" file. Put it in the "wasm-stub" namespace to tell
// our stub load callback to fill it with JavaScript.
//
// Resolve relative paths to absolute paths here since this
// resolve callback is given "resolveDir", the directory to
// resolve imports against.
if (args.resolveDir === '') {
return // Ignore unresolvable paths
}
return {
path: path.isAbsolute(args.path) ? args.path : path.join(args.resolveDir, args.path),
namespace: 'wasm-stub',
}
})
// Virtual modules in the "wasm-stub" namespace are filled with
// the JavaScript code for compiling the WebAssembly binary. The
// binary itself is imported from a second virtual module.
build.onLoad({ filter: /.*/, namespace: 'wasm-stub' }, async (args) => ({
contents: `import wasm from ${JSON.stringify(args.path)}
export default (imports) =>
WebAssembly.instantiate(wasm, imports).then(
result => result.instance.exports)`,
}))
// Virtual modules in the "wasm-binary" namespace contain the
// actual bytes of the WebAssembly file. This uses esbuild's
// built-in "binary" loader instead of manually embedding the
// binary data inside JavaScript code ourselves.
build.onLoad({ filter: /.*/, namespace: 'wasm-binary' }, async (args) => ({
contents: await fs.promises.readFile(args.path),
loader: 'binary',
}))
},
}
await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
outfile: 'out.js',
plugins: [wasmPlugin],
})
package main
import "encoding/json"
import "io/ioutil"
import "os"
import "path/filepath"
import "github.com/evanw/esbuild/pkg/api"
var wasmPlugin = api.Plugin{
Name: "wasm",
Setup: func(build api.PluginBuild) {
// Resolve ".wasm" files to a path with a namespace
build.OnResolve(api.OnResolveOptions{Filter: `\.wasm$`},
func(args api.OnResolveArgs) (api.OnResolveResult, error) {
// If this is the import inside the stub module, import the
// binary itself. Put the path in the "wasm-binary" namespace
// to tell our binary load callback to load the binary file.
if args.Namespace == "wasm-stub" {
return api.OnResolveResult{
Path: args.Path,
Namespace: "wasm-binary",
}, nil
}
// Otherwise, generate the JavaScript stub module for this
// ".wasm" file. Put it in the "wasm-stub" namespace to tell
// our stub load callback to fill it with JavaScript.
//
// Resolve relative paths to absolute paths here since this
// resolve callback is given "resolveDir", the directory to
// resolve imports against.
if args.ResolveDir == "" {
return api.OnResolveResult{}, nil // Ignore unresolvable paths
}
if !filepath.IsAbs(args.Path) {
args.Path = filepath.Join(args.ResolveDir, args.Path)
}
return api.OnResolveResult{
Path: args.Path,
Namespace: "wasm-stub",
}, nil
})
// Virtual modules in the "wasm-stub" namespace are filled with
// the JavaScript code for compiling the WebAssembly binary. The
// binary itself is imported from a second virtual module.
build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: "wasm-stub"},
func(args api.OnLoadArgs) (api.OnLoadResult, error) {
bytes, err := json.Marshal(args.Path)
if err != nil {
return api.OnLoadResult{}, err
}
contents := `import wasm from ` + string(bytes) + `
export default (imports) =>
WebAssembly.instantiate(wasm, imports).then(
result => result.instance.exports)`
return api.OnLoadResult{Contents: &contents}, nil
})
// Virtual modules in the "wasm-binary" namespace contain the
// actual bytes of the WebAssembly file. This uses esbuild's
// built-in "binary" loader instead of manually embedding the
// binary data inside JavaScript code ourselves.
build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: "wasm-binary"},
func(args api.OnLoadArgs) (api.OnLoadResult, error) {
bytes, err := ioutil.ReadFile(args.Path)
if err != nil {
return api.OnLoadResult{}, err
}
contents := string(bytes)
return api.OnLoadResult{
Contents: &contents,
Loader: api.LoaderBinary,
}, nil
})
},
}
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Outfile: "out.js",
Plugins: []api.Plugin{wasmPlugin},
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
该插件分多个步骤工作。首先,一个解析回调捕获普通模块中的.wasm
路径并将它们移动到wasm-stub
命名空间。然后,wasm-stub
命名空间的加载回调生成一个 JavaScript 存根模块,该模块导出加载器函数并导入.wasm
路径。这将再次调用解析回调,这次将路径移动到wasm-binary
命名空间。然后,wasm-binary
命名空间的第二个加载回调会导致使用binary
加载器加载 WebAssembly 文件,这告诉 esbuild 将文件本身嵌入到捆绑包中。
#Svelte 插件
此示例演示:支持编译到 JavaScript 的语言、报告警告和错误、集成源映射。
此插件允许您捆绑.svelte
文件,这些文件来自Svelte框架。您以类似 HTML 的语法编写代码,然后由 Svelte 编译器将其转换为 JavaScript。Svelte 代码看起来像这样
<script>
let a = 1;
let b = 2;
</script>
<input type="number" bind:value={a}>
<input type="number" bind:value={b}>
<p>{a} + {b} = {a + b}</p>
使用 Svelte 编译器编译此代码会生成一个 JavaScript 模块,该模块依赖于svelte/internal
包,并将组件作为使用default
导出导出的单个类导出。这意味着.svelte
文件可以独立编译,这使得 Svelte 非常适合 esbuild 插件。此插件由导入.svelte
文件触发,如下所示
import Button from './button.svelte'
以下是插件的代码(没有此插件的 Go 版本,因为 Svelte 编译器是用 JavaScript 编写的)
import * as esbuild from 'esbuild'
import * as svelte from 'svelte/compiler'
import path from 'node:path'
import fs from 'node:fs'
let sveltePlugin = {
name: 'svelte',
setup(build) {
build.onLoad({ filter: /\.svelte$/ }, async (args) => {
// This converts a message in Svelte's format to esbuild's format
let convertMessage = ({ message, start, end }) => {
let location
if (start && end) {
let lineText = source.split(/\r\n|\r|\n/g)[start.line - 1]
let lineEnd = start.line === end.line ? end.column : lineText.length
location = {
file: filename,
line: start.line,
column: start.column,
length: lineEnd - start.column,
lineText,
}
}
return { text: message, location }
}
// Load the file from the file system
let source = await fs.promises.readFile(args.path, 'utf8')
let filename = path.relative(process.cwd(), args.path)
// Convert Svelte syntax to JavaScript
try {
let { js, warnings } = svelte.compile(source, { filename })
let contents = js.code + `//# sourceMappingURL=` + js.map.toUrl()
return { contents, warnings: warnings.map(convertMessage) }
} catch (e) {
return { errors: [convertMessage(e)] }
}
})
}
}
await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
outfile: 'out.js',
plugins: [sveltePlugin],
})
此插件只需要一个加载回调,不需要解析回调,因为它很简单,只需要将加载的代码转换为 JavaScript,而无需担心代码来自哪里。
它将//# sourceMappingURL=
注释附加到生成的 JavaScript,以告诉 esbuild 如何将生成的 JavaScript 映射回原始源代码。如果在构建期间启用了源映射,esbuild 将使用它来确保最终源映射中生成的职位被映射回原始 Svelte 文件,而不是映射到中间 JavaScript 代码。
#插件 API 限制
此 API 不打算涵盖所有用例。无法挂钩到捆绑过程的每个部分。例如,目前无法直接修改 AST。此限制的存在是为了保持 esbuild 的出色性能特征,以及避免公开过多的 API 表面,这将是维护负担,并将阻止涉及更改 AST 的改进。
可以将 esbuild 视为 Web 的“链接器”。就像本机代码的链接器一样,esbuild 的工作是获取一组文件,解析和绑定它们之间的引用,并生成包含所有链接在一起的代码的单个文件。插件的工作是生成最终被链接的单个文件。
esbuild 中的插件在范围相对较小且仅自定义构建的某个小方面时效果最佳。例如,用于自定义格式(例如 YAML)的特殊配置文件的插件非常合适。您使用的插件越多,构建速度就越慢,尤其是在插件是用 JavaScript 编写的。如果插件应用于构建中的每个文件,那么您的构建速度可能会非常慢。如果适用缓存,则必须由插件本身完成。