插件
插件API允许你在构建过程的各个部分注入代码。与其他API不同,它不能从命令行使用。 你必须编写JavaScript或Go代码才能使用插件API。插件也只能与build API一起使用, 而不能与transform API一起使用。
#查找插件
如果你正在寻找现有的esbuild插件,你应该查看 现有esbuild插件列表。 该列表中的插件都是作者有意添加的,旨在供esbuild社区中的其他人使用。
如果你想分享你的esbuild插件,你应该:
- 发布到npm 以便其他人可以安装它。
- 将其添加到现有esbuild插件列表 以便其他人可以找到它。
#使用插件
esbuild插件是一个具有name
和setup
函数的对象。它们以数组形式传递给 build API调用。setup
函数在每次build 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)
}
}
You would use it like this:
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 callbacks - 解析回调
使用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;
with: Record<string, string>;
}
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{}
With map[string]string
}
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
这是用于解析导入路径的文件系统目录。
kind
这表示要解析的路径是如何被导入的。例如,
'entry-
表示该路径是作为 入口点路径提供给API的,point' 'import-
表示该路径来自JavaScript的statement' import
或export
语句,而'import-
表示该路径来自CSS的rule' @import
规则。pluginData
这个属性是从前一个插件传递过来的,由加载此文件的on-load回调设置。
with
这包含了用于导入此模块的导入语句中存在的导入属性 映射。例如,使用
with {
导入的模块将向插件提供type: 'json' } { type:
的'json' } with
值。你可以使用这个根据导入属性解析到不同的路径。
#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
将此设置为非空字符串以将导入解析到特定路径。如果设置了此项,则不会为此模块中的此导入路径 运行更多的on-resolve回调。如果未设置此项,esbuild将继续运行在当前回调之后注册的on-resolve 回调。然后,如果路径仍未解析,esbuild将默认相对于当前模块的解析目录解析路径。
external
将此设置为
true
以将模块标记为外部模块,这意味着它不会被包含在 打包中,而是在运行时导入。namespace
这是与解析路径关联的命名空间。如果留空,对于非外部路径将默认为
file
命名空间。file命名空间 中的路径必须是当前文件系统的绝对路径(在Unix上以正斜杠开头,在Windows上以驱动器字母开头)。如果你想解析到一个不是文件系统路径的路径,你应该将命名空间设置为
file
或空字符串以外的值。 这告诉esbuild不要将该路径视为指向文件系统上的某个内容。errors
和warnings
这些属性让你可以将在路径解析期间生成的任何日志消息传递给esbuild,这些消息将根据当前的 日志级别在终端中显示,并最终出现在构建结果中。例如,如果你正在调用 一个库,并且该库可以返回错误和/或警告,你会想要使用这些属性转发它们。
如果你只有一个错误要返回,你不必通过
errors
传递它。你可以在JavaScript中直接抛出错误, 或在Go中将error
对象作为第二个返回值返回。watchFiles
和watchDirs
这些属性让你可以为esbuild的监视模式返回额外的文件系统路径进行扫描。 默认情况下,esbuild只会扫描提供给
onLoad
插件的路径,并且仅当命名空间为file
时才会扫描。 如果你的插件需要对文件系统中的其他更改做出反应,它需要使用这些属性之一。如果自上次构建以来
watchFiles
数组中的任何文件发生了更改,将触发重新构建。更改检测 比较复杂,可能会检查文件内容和/或文件的元数据。如果自上次构建以来
watchDirs
数组中任何目录的目录条目列表发生了更改,也会触发重新构建。 请注意,这不会检查这些目录中任何文件的内容,也不会检查任何子目录。可以将其视为检查Unixls
命令的输出。为了保证健壮性,你应该包含在插件评估期间使用的所有文件系统路径。例如,如果你的插件执行 类似于
require.resolve()
的操作,你需要包含所有"此文件是否存在"检查的路径,而不仅仅是 最终路径。否则,可能会创建一个新文件导致构建过时,但esbuild无法检测到它,因为该路径 未被列出。pluginName
此属性允许你将此插件的名称替换为此路径解析操作的另一个名称。这对于通过此插件代理另一个 插件很有用。例如,它允许你有一个单独的插件,该插件转发到包含多个插件的子进程。你可能 不需要使用这个。
pluginData
这个属性将被传递给插件链中运行的下一个插件。如果你从
onLoad
插件返回它,它将被传递给 该文件中任何导入的onResolve
插件,如果你从onResolve
插件返回它,一个任意插件将 被传递给onLoad
插件,因为它们之间的关系是多对一的。这对于在不同插件之间传递数据很有用。sideEffects
设置此属性为false告诉esbuild,如果导入的名称未被使用,则可以删除此模块的导入。这就像 在相应的
package.json
文件中指定了"sideEffects": false
一样。例如,import { x }
可能完全被删除,如果from "y" x
未被使用且y
已被标记为sideEffects: false
。你可以阅读更多关于sideEffects
含义的信息,在Webpack的文档关于此功能的介绍。suffix
在这里返回一个值让你可以传递一个可选的URL查询或散列,附加到路径上,而不是包含在路径本身。 将此单独存储是有益的,因为在某些情况下,路径被某些东西处理时,可能无法识别后缀, 无论是通过esbuild本身还是通过另一个插件。
例如,一个on-resolve插件可能会为构建中不同on-load插件的结尾返回一个后缀
?#iefix
, 保持后缀分离意味着后缀仍然与路径相关,但.eot
插件仍然可以在不了解后缀的情况下匹配文件。如果你确实设置了后缀,它必须以
?
或#
开头,因为它被设计为URL查询或散列。此功能具有某些 隐晦的用途,例如在IE8的CSS解析器中的错误,可能不是很有用。如果你确实使用它,请记住, 每个唯一的命名空间、路径和后缀组合都被esbuild视为唯一的模块标识符,因此为相同路径返回不同的后缀, 你是在告诉esbuild创建另一个模块的副本。
#On-load callbacks
使用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.
),你应该将回调设为async
并使用await
(在这种情况下使用fs.
), 以允许其他代码在此期间运行。在Go中,每个回调可能在单独的goroutine上运行。如果你的 插件使用任何共享数据结构,请确保有适当的同步机制。
#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
pluginData
This property is passed from the previous plugin, as set by the on-resolve callback that runs in the plugin chain.
with
This contains a map of the import attributes that were present on the import statement used to import this module. For example, a module imported using
with {
will provide atype: 'json' } with
value of{ type:
to plugins. A given module is loaded separately for each unique combination of import attributes, so these attributes are guaranteed to have been provided by all import statements used to import this module. That means they can be used by the plugin to alter the content of this module.'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
Set this to a string to specify the contents of the module. If this is set, no more on-load callbacks will be run for this resolved path. If this is not set, esbuild will continue to run on-load callbacks that were registered after the current one. Then, if the contents are still not set, esbuild will default to loading the contents from the file system if the resolved path is in the
file
namespace.loader
This tells esbuild how to interpret the contents. For example, the
js
loader interprets the contents as JavaScript and thecss
loader interprets the contents as CSS. The loader defaults tojs
if it's not specified. See the content types page for a complete list of all built-in loaders.resolveDir
This is the file system directory to use when resolving an import path in this module to a real path on the file system. For modules in the
file
namespace, this value defaults to the directory part of the module path. Otherwise this value defaults to empty unless the plugin provides one. If the plugin doesn't provide one, esbuild's default behavior won't resolve any imports in this module. This directory will be passed to any on-resolve callbacks that run on unresolved import paths in this module.errors
andwarnings
These properties let you pass any log messages generated during path resolution to esbuild where they will be displayed in the terminal according to the current log level and end up in the final build result. For example, if you are calling a library and that library can return errors and/or warnings, you will want to forward them using these properties.
If you only have a single error to return, you don't have to pass it via
errors
. You can simply throw the error in JavaScript or return theerror
object as the second return value in Go.watchFiles
andwatchDirs
These properties let you return additional file system paths for esbuild's watch mode to scan. By default esbuild will only scan the path provided to
onLoad
plugins, and only if the namespace isfile
. If your plugin needs to react to additional changes in the file system, it needs to use one of these properties.A rebuild will be triggered if any file in the
watchFiles
array has been changed since the last build. Change detection is somewhat complicated and may check the file contents and/or the file's metadata.A rebuild will also be triggered if the list of directory entries for any directory in the
watchDirs
array has been changed since the last build. Note that this does not check anything about the contents of any file in these directories, and it also does not check any subdirectories. Think of this as checking the output of the Unixls
command.For robustness, you should include all file system paths that were used during the evaluation of the plugin. For example, if your plugin does something equivalent to
require.resolve()
, you'll need to include the paths of all "does this file exist" checks, not just the final path. Otherwise a new file could be created that causes the build to become outdated, but esbuild doesn't detect it because that path wasn't listed.pluginName
This property lets you replace this plugin's name with another name for this module load operation. It's useful for proxying another plugin through this plugin. For example, it lets you have a single plugin that forwards to a child process containing multiple plugins. You probably won't need to use this.
pluginData
This property will be passed to the next plugin that runs in the plugin chain. If you return it from an
onLoad
plugin, it will be passed to theonResolve
plugins for any imports in that file, and if you return it from anonResolve
plugin, an arbitrary one will be passed to theonLoad
plugin when it loads the file (it's arbitrary since the relationship is many-to-one). This is useful to pass data between different plugins without them having to coordinate directly.
#Caching your plugin
Since esbuild is so fast, it's often the case that plugin evaluation is the main bottleneck when building with esbuild. Caching of plugin evaluation is left up to each plugin instead of being a part of esbuild itself because cache invalidation is plugin-specific. If you are writing a slow plugin that needs a cache to be fast, you will have to write the cache logic yourself.
A cache is essentially a map that memoizes the transform function that represents your plugin. The keys of the map usually contain the inputs to your transform function and the values of the map usually contain the outputs of your transform function. In addition, the map usually has some form of least-recently-used cache eviction policy to avoid continually growing larger in size over time.
The cache can either be stored in memory (beneficial for use with esbuild's rebuild API), on disk (beneficial for caching across separate build script invocations), or even on a server (beneficial for really slow transforms that can be shared between different developer machines). Where to store the cache is case-specific and depends on your plugin.
Here is a simple caching example. Say we want to cache the function slowTransform()
that takes as input the contents of a file in the *.example
format and transforms it to JavaScript. An in-memory cache that avoids redundant calls to this function when used with esbuild's rebuild API) might look something like this:
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
})
}
}
Some important caveats about the caching code above:
There is no cache eviction policy present in the code above. Memory usage will continue to grow if more and more keys are added to the cache map.
input
值存储在缓存value
中,而不是缓存key
中。 这意味着更改文件内容不会泄漏内存,因为键只包含文件路径,而不包含文件内容。更改文件 内容只会覆盖之前的缓存条目。对于常见用法来说,这可能是可以的,比如有人在增量重建 之间重复编辑同一个文件,并且只是偶尔添加或重命名文件。缓存失效只有在
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处理 semver的方式,即使你已固定包的版本,其传递依赖也可能已更新,或者有人可能在此期间 修改了包内容,或者转换函数可能正在 调用node API,不同的构建可能在不同的node版本上运行。如果你想将缓存存储在文件系统上,你应该通过在缓存键中存储转换函数代码的某种表示来 防止对转换函数代码的更改。这通常是某种形式的哈希, 它包含所有相关包中所有相关文件的内容,以及可能的其他细节,如你当前运行的node版本。 让所有这些都正确是非常复杂的。
#On-start回调
注册一个on-start回调以在新构建开始时得到通知。这对所有构建都会触发,而不仅仅是初始 构建,所以它对于重建、监视模式和服务模式 特别有用。以下是如何添加on-start回调:
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() {
}
你不应该使用on-start回调进行初始化,因为它可能会运行多次。如果你想初始化某些内容, 只需将插件初始化代码直接放在setup
函数内即可。
on-start回调可以是async
的,并且可以返回一个promise。所有插件的所有on-start回调 都会并发运行,然后构建会等待所有on-start回调完成后再继续。on-start回调可以选择性地 返回要包含在构建中的错误和/或警告。
Note that on-start callbacks do not have the ability to mutate the build options. The initial build options can only be modified within the setup
function and are consumed once setup
returns. All builds after the first one reuse the same initial options so the initial options are never re-consumed, and modifications to build.initialOptions
that are done within the start callback are ignored.
#On-end callbacks
Register an on-end callback to be notified when a new build ends. This triggers for all builds, not just the initial build, so it's especially useful for rebuilds, watch mode, and serve mode. Here's how to add an on-end callback:
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() {
}
所有on-end回调都按顺序运行,每个回调都可以访问最终的构建结果。它可以在返回之前修改构建 结果,并可以通过返回promise来延迟构建的结束。如果你想能够检查构建图,你应该在初始选项 上启用metafile设置,构建图将作为构建结果对象上的metafile
属性返回。
#On-dispose回调
注册一个on-dispose回调以在插件不再使用时执行清理。它将在每次build()
调用后被调用, 无论构建是否失败,以及在给定构建上下文的第一次dispose()
调用之后被调用。以下是如何 添加on-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的一些额外信息:
If you don't pass the optional
resolveDir
parameter, esbuild will still runonResolve
plugin callbacks but will not attempt any path resolution itself. All of esbuild's path resolution logic depends on theresolveDir
parameter including looking for packages innode_modules
directories (since it needs to know where thosenode_modules
directories might be).如果你想在特定目录中解析文件名,请确保输入路径以
./
开头。否则,输入路径将被视为 包路径而不是相对路径。这种行为与esbuild的正常路径解析逻辑相同。如果路径解析失败,返回对象上的
errors
属性将是一个非空数组,包含错误信息。这个函数 在失败时不总是抛出错误。你需要在调用它之后检查是否有错误。这个函数的行为取决于构建配置。这就是为什么它是
build
对象的属性而不是顶级API调用。 这也意味着你不能在所有插件的setup
函数完成之前调用它,因为这些函数给插件机会在构建 开始时冻结之前调整构建配置。所以resolve
函数在你的onResolve
和/或onLoad
回调 内部最有用。目前没有尝试检测无限路径解析循环。在
onResolve
中使用相同参数调用resolve
几乎肯定 是一个坏主意。
#解析选项
resolve
函数将要解析的路径作为第一个参数,将具有可选属性的对象作为第二个参数。这个 选项对象与传递给onResolve
的参数非常相似。以下是可用的选项:
interface ResolveOptions {
kind: ResolveKind;
importer?: string;
namespace?: string;
resolveDir?: string;
pluginData?: any;
with?: Record<string, string>;
}
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{}
With map[string]string
}
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
如果设置,这被解释为包含要解析的导入的模块的路径。这会影响检查
importer
值的onResolve
回调的插件。namespace
如果设置,这被解释为包含要解析的导入的模块的命名空间。这会影响检查
namespace
值的onResolve
回调的插件。你可以在这里阅读更多关于命名空间的信息。resolveDir
这是用于将导入路径解析为文件系统上的实际路径的文件系统目录。即使对于非相对包路径, 这也必须设置,以便esbuild的内置路径解析能够找到给定的文件(因为esbuild需要知道
node_modules
目录在哪里)。pluginData
这个属性可以用来将自定义数据传递给匹配此导入路径的on-resolve回调。 这些数据的含义完全由你决定。
with
这是与此路径的导入语句相关联的导入属性。 例如,对于使用
with {
属性导入的模块,适当的type: 'json' } with
值 应该是{ type:
。esbuild不使用这些信息,但on-resolve回调 可能会使用它们。'json' }
#解析结果
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
这是路径解析的结果,如果路径解析失败则为空字符串。
external
如果路径被标记为外部,则此值为
true
,这意味着它不会被包含在 包中,而是在运行时导入。namespace
这是与解析路径关联的命名空间。你可以在这里阅读更多关于命名空间的信息。
errors
和warnings
这些属性包含在路径解析期间生成的任何日志消息,可能来自响应此路径解析操作的任何插件 或esbuild本身。这些日志消息不会自动包含在日志中,所以如果你丢弃它们,它们将完全不可见。 如果你想让它们包含在日志中,你需要从
onResolve
或onLoad
返回它们。pluginData
如果插件响应了此路径解析操作并从其
onResolve
回调返回了pluginData
,那些数据将 最终出现在这里。这对于在不同插件之间传递数据而不需要它们直接协调很有用。sideEffects
除非模块以某种方式被注释为没有副作用,否则此属性将为
true
,在这种情况下它将为false
。 对于在相应的package.json
文件中有"sideEffects": false
的包,以及如果插件响应此路径 解析操作并返回sideEffects: false
时,这将为false
。你可以在Webpack的文档关于此功能的介绍 中阅读更多关于sideEffects
的含义。suffix
如果要解析的路径末尾有URL查询或散列,并且如果需要删除它才能成功解析路径,这里可以 包含一个可选的URL查询或散列。
#Example plugins - 示例插件
以下示例插件旨在让你了解使用插件API可以做的不同类型的事情。
#HTTP plugin - HTTP插件
本示例演示了:使用文件系统路径以外的路径格式、特定命名空间的路径解析、 结合使用resolve和load回调。
此插件允许你在JavaScript代码中导入HTTP URL。代码将在构建时自动下载。 它启用了以下工作流程:
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) {
// 拦截以"http:"和"https:"开头的导入路径,这样
// esbuild就不会尝试将它们映射到文件系统位置。
// 用"http-url"命名空间标记它们,以将它们与
// 此插件关联。
build.onResolve({ filter: /^https?:\/\// }, args => ({
path: args.path,
namespace: 'http-url',
}))
// 我们还想拦截下载文件中的所有导入路径,并根据原始URL
// 解析它们。所有这些文件都将在"http-url"命名空间中。
// 确保将新解析的URL保持在"http-url"命名空间中,这样
// 其中的导入也会递归地解析为URL。
build.onResolve({ filter: /.*/, namespace: 'http-url' }, args => ({
path: new URL(args.path, args.importer).toString(),
namespace: 'http-url',
}))
// 当加载URL时,我们想要实际从互联网下载内容。这里只有
// 足够的逻辑来处理来自unpkg.com的示例导入,但在实际
// 情况下,这可能需要更复杂的处理。
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) {
// 拦截以"http:"和"https:"开头的导入路径,这样
// esbuild就不会尝试将它们映射到文件系统位置。
// 用"http-url"命名空间标记它们,以将它们与
// 此插件关联。
build.OnResolve(api.OnResolveOptions{Filter: `^https?://`},
func(args api.OnResolveArgs) (api.OnResolveResult, error) {
return api.OnResolveResult{
Path: args.Path,
Namespace: "http-url",
}, nil
})
// 我们还想拦截下载文件中的所有导入路径
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
})
// 当加载URL时,我们想要实际从互联网下载内容。这里只有
// 足够的逻辑来处理来自unpkg.com的示例导入,但在实际
// 情况下,这可能需要更复杂的处理。
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插件
此示例演示了:处理二进制数据、使用导入语句创建虚拟模块、使用不同命名空间重用相同路径。
此插件允许你在JavaScript代码中导入.wasm
文件。它不生成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的内置binary加载器将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编译器编译此代码会生成一个依赖于svelte/internal
包的JavaScript模块, 并使用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,而不用担心代码来自哪里。
它在生成的JavaScript后面附加一个//# sourceMappingURL=
注释,以告诉esbuild如何将 生成的JavaScript映射回原始源代码。如果在构建期间启用了源映射,esbuild将使用这个 来确保最终源映射中的生成位置被映射回原始Svelte文件,而不是映射到中间JavaScript代码。
It appends a //# sourceMappingURL=
comment to the generated JavaScript to tell esbuild how to map the generated JavaScript back to the original source code. If source maps are enabled during the build, esbuild will use this to ensure that the generated positions in the final source map are mapped all the way back to the original Svelte file instead of to the intermediate JavaScript code.
#Plugin API limitations
此API不打算覆盖所有用例。无法钩入打包过程的每个部分。例如,目前无法直接修改AST。 这个限制的存在是为了保持esbuild出色的性能特征,以及避免暴露太多的API表面,这会 成为维护负担,并会阻止涉及更改AST的改进。
可以将esbuild视为Web的"链接器"。就像本地代码的链接器一样,esbuild的工作是获取一组 文件,解析和绑定它们之间的引用,并生成一个包含所有链接在一起的代码的单个文件。 插件的工作是生成最终被链接的单个文件。
esbuild中的插件在相对范围较小且只自定义构建的一小部分时效果最好。例如,用于自定义 格式(例如YAML)的特殊配置文件的插件是非常合适的。使用的插件越多,构建速度就会越慢, 特别是如果你的插件是用JavaScript编写的。如果插件适用于构建中的每个文件,那么你的 构建可能会非常慢。如果适用缓存,则必须由插件本身完成。