插件

插件 API 允许你将代码注入构建过程的各个部分。与 API 的其他部分不同,它无法从命令行使用。你必须编写 JavaScript 或 Go 代码才能使用插件 API。插件也只适用于 build API,不适用于 transform API。

查找插件

如果你正在寻找现有的 esbuild 插件,你应该查看 现有的 esbuild 插件列表。此列表中的插件是由作者故意添加的,旨在供 esbuild 社区中的其他人使用。

如果你想分享你的 esbuild 插件,你应该

  1. 发布到 npm 以便其他人可以安装它。
  2. 将其添加到 现有的 esbuild 插件列表 以便其他人可以找到它。

使用插件

esbuild 插件是一个具有 namesetup 函数的对象。它们被传递到 build API 调用的数组中。setup 函数在每次构建 API 调用时运行一次。

这是一个简单的插件示例,它允许你在构建时导入当前的环境变量

JS Go
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 如何进行路径解析。例如,它可以拦截导入路径并将它们重定向到其他地方。它还可以将路径标记为外部。这是一个例子

JS Go
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.existsSync(),你应该使回调 async 并使用 await(在这种情况下使用 fs.promises.exists())以允许其他代码在同时运行。在 Go 中,每个回调都可能在单独的 goroutine 上运行。如果你的插件使用任何共享数据结构,请确保你已到位适当的同步。

On-resolve 选项

onResolve API 旨在在 setup 函数中调用,并注册一个回调,以便在某些情况下触发。它接受一些选项

JS Go
interface OnResolveOptions {
  filter: RegExp;
  namespace?: string;
}
type OnResolveOptions struct {
  Filter    string
  Namespace string
}

On-resolve 参数

当 esbuild 调用由 onResolve 注册的回调时,它将提供这些参数,其中包含有关导入路径的信息

JS Go
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
)

On-resolve 结果

这是可以使用 onResolve 添加的回调返回的对象,以提供自定义路径解析。如果你想从回调中返回而不提供路径,只需返回默认值(因此在 JavaScript 中为 undefined,在 Go 中为 OnResolveResult{})。以下是可以返回的可选属性

JS Go
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
}

On-load 回调

使用 `onLoad` 添加的回调将针对每个尚未标记为外部的唯一路径/命名空间对运行。它的作用是返回模块的内容并告诉 esbuild 如何解释它。以下是一个将 `txt` 文件转换为单词数组的示例插件

JS Go
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.readFileSync()`,您应该使回调 `async` 并使用 `await`(在这种情况下使用 `fs.promises.readFile()`)以允许其他代码在同时运行。在 Go 中,每个回调都可能在单独的 goroutine 上运行。如果您的插件使用任何共享数据结构,请确保您已到位适当的同步。

On-load 选项

`onLoad` API 旨在在 `setup` 函数中调用,并注册一个回调以在某些情况下触发。它需要一些选项

JS Go
interface OnLoadOptions {
  filter: RegExp;
  namespace?: string;
}
type OnLoadOptions struct {
  Filter    string
  Namespace string
}

On-load 参数

当 esbuild 调用由 `onLoad` 注册的回调时,它将提供这些参数,其中包含有关要加载的模块的信息

JS Go
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
}

On-load 结果

这是可以使用 `onLoad` 添加的回调返回的对象,以提供模块的内容。如果您想从回调中返回而不提供任何内容,只需返回默认值(因此在 JavaScript 中为 `undefined`,在 Go 中为 `OnLoadResult{}`)。以下是可以返回的可选属性

JS Go
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
}

缓存您的插件

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

关于上面的缓存代码的一些重要注意事项

启动时回调

注册一个启动时回调,以便在新的构建开始时收到通知。这会触发所有构建,而不仅仅是初始构建,因此它对于 重新构建监视模式服务模式 特别有用。以下是添加启动时回调的方法

JS Go
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 的修改会被忽略。

结束时回调

注册一个结束时回调,以便在新的构建结束时收到通知。这会触发所有构建,而不仅仅是初始构建,因此它对于 重新构建监视模式服务模式 特别有用。以下是添加结束时回调的方法

JS Go
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() 调用后。以下是添加销毁时回调的方法

JS Go
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 方法中访问初始构建选项。这使你能够检查构建的配置方式,以及在构建开始之前修改构建选项。以下是一个示例

JS Go
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 的路径解析的输入和/或输出。以下是一个示例

JS Go
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 的其他注意事项

解析选项

resolve 函数将要解析的路径作为第一个参数,并将一个包含可选属性的对象作为第二个参数。此选项对象与 传递给 onResolve 的参数 非常相似。以下是可用的选项

JS Go
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
)

解析结果

resolve 函数返回一个对象,该对象与插件可以 onResolve 回调返回的对象 非常相似。它具有以下属性

JS Go
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
}

示例插件

以下示例插件旨在让您了解使用插件 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']))

可以使用以下插件来实现。请注意,对于实际使用,下载应该被缓存,但为了简洁起见,本示例中省略了缓存

JS Go
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文件实际上会生成两个虚拟模块。以下是插件的代码

JS Go
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 编写的。如果插件应用于构建中的每个文件,那么您的构建速度可能会非常慢。如果适用缓存,则必须由插件本身完成。