外掛程式

外掛程式 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)
  }
}

您會像這樣使用它

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 上執行。如果你的外掛程式使用任何共用資料結構,請務必建立適當的同步機制。

路徑解析選項

onResolve API 旨在在 setup 函式內呼叫,並註冊回呼函式以在特定情況下觸發。它採用幾個選項

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

路徑解析引數

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

解析結果

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

載入時回呼

使用 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 上執行。如果您外掛程式使用任何共用資料結構,請確保您有適當的同步機制。

載入選項

onLoad API 旨在在 setup 函式中呼叫,並註冊一個回呼,以便在特定情況下觸發。它需要一些選項

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

載入參數

當 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
}

載入結果

這是使用 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 的 rebuild API 搭配使用)、磁碟上(有利於在不同的建置指令碼呼叫之間進行快取),甚至在伺服器上(有利於在不同的開發人員電腦之間共用非常慢的轉換)。快取儲存的位置取決於具體情況,並取決於您的外掛程式。

以下是一個簡單的快取範例。假設我們要快取函式 slowTransform(),它將 *.example 格式的檔案內容作為輸入,並將其轉換為 JavaScript。與 esbuild 的 rebuild 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`,並且可以傳回承諾。來自所有外掛程式的啟動時回呼會同時執行,然後建置會等到所有啟動時回呼完成後再繼續進行。啟動時回呼可以選擇傳回錯誤和/或警告,以包含在建置中。

請注意,啟動時回呼無法變更 建置選項。初始建置選項只能在 `setup` 函式中修改,並且在 `setup` 傳回後會使用一次。第一個建置之後的所有建置都會重複使用相同的初始選項,因此初始選項永遠不會被重新使用,而且在啟動時回呼中對 `build.initialOptions` 所做的修改都會被忽略。

結束時回呼

註冊一個 on-end 回呼,以便在新的建置結束時收到通知。這會觸發所有建置,不只是初始建置,因此對於 重建監控模式提供服務模式 特別有用。以下是新增 on-end 回呼的方法

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 回呼都會以串列方式執行,且每個回呼都會取得最終建置結果的存取權限。它可以在回傳前修改建置結果,並透過回傳承諾來延遲建置結束。如果您想要檢查建置圖表,您應該在 初始選項 上啟用 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;
}

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 模組的函式,該模組已匯出為預設匯出。該 stub 模組看起來像這樣

import wasm from '/path/to/example.wasm'
export default (imports) =>
  WebAssembly.instantiate(wasm, imports).then(
    result => result.instance.exports)

然後,該 stub 模組使用 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 stub 模組,該模組會匯出載入器函式並匯入 .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 的一種方式是將其視為網路的「連結器」。就像原生程式碼的連結器一樣,esbuild 的工作是取得一組檔案,解析並繫結它們之間的參照,並產生一個包含所有連結在一起的程式碼的單一檔案。外掛程式的任務是產生最終連結的個別檔案。

esbuild 中的外掛程式在範圍相對較小且僅自訂建置的一小部分時,會發揮最佳效能。例如,用於自訂格式(例如 YAML)中的特殊設定檔的外掛程式非常合適。使用的外掛程式越多,建置速度就會越慢,特別是如果外掛程式是用 JavaScript 編寫的。如果外掛程式套用於建置中的每個檔案,則建置速度可能會非常慢。如果適用快取,則必須由外掛程式本身執行。