插件

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

查找插件

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

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

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

使用插件

esbuild插件是一个具有namesetup函数的对象。它们以数组形式传递给 build API调用。setup函数在每次build 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)
  }
}

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

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;
  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
)

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 callbacks

使用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
}

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:

On-start回调

注册一个on-start回调以在新构建开始时得到通知。这对所有构建都会触发,而不仅仅是初始 构建,所以它对于重建监视模式服务模式 特别有用。以下是如何添加on-start回调:

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() {
}

你不应该使用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:

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() {
}

所有on-end回调都按顺序运行,每个回调都可以访问最终的构建结果。它可以在返回之前修改构建 结果,并可以通过返回promise来延迟构建的结束。如果你想能够检查构建图,你应该在初始选项 上启用metafile设置,构建图将作为构建结果对象上的metafile属性返回。

On-dispose回调

注册一个on-dispose回调以在插件不再使用时执行清理。它将在每次build()调用后被调用, 无论构建是否失败,以及在给定构建上下文的第一次dispose()调用之后被调用。以下是如何 添加on-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;
  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
)

解析结果

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
}

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']))

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

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

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编译器编译此代码会生成一个依赖于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编写的。如果插件适用于构建中的每个文件,那么你的 构建可能会非常慢。如果适用缓存,则必须由插件本身完成。