外掛程式
外掛程式 API 允許您將程式碼插入建立程式的各個部分。與 API 的其他部分不同,它無法從命令列使用。您必須撰寫 JavaScript 或 Go 程式碼才能使用外掛程式 API。外掛程式也只可用於 build API,而不能用於 transform API。
#尋找外掛程式
如果您正在尋找現有的 esbuild 外掛程式,您應該查看 現有 esbuild 外掛程式清單。此清單上的外掛程式是由作者故意加入的,並供 esbuild 社群中的其他人使用。
如果您想要分享您的 esbuild 外掛程式,您應該
- 將其發佈到 npm,以便其他人可以安裝它。
- 將它新增到 現有 esbuild 外掛程式清單,以便其他人可以找到它。
#使用外掛程式
esbuild 外掛程式是一個具有 name
和 setup
函式的物件。它們會以陣列傳遞給 build API 呼叫。setup
函式會針對每個 build API 呼叫執行一次。
以下是允許您在建置時匯入目前環境變數的簡單外掛程式範例
import * as esbuild from 'esbuild'
let envPlugin = {
name: 'env',
setup(build) {
// Intercept import paths called "env" so esbuild doesn't attempt
// to map them to a file system location. Tag them with the "env-ns"
// namespace to reserve them for this plugin.
build.onResolve({ filter: /^env$/ }, args => ({
path: args.path,
namespace: 'env-ns',
}))
// Load paths tagged with the "env-ns" namespace and behave as if
// they point to a JSON file containing the environment variables.
build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => ({
contents: JSON.stringify(process.env),
loader: 'json',
}))
},
}
await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
outfile: 'out.js',
plugins: [envPlugin],
})
package main
import "encoding/json"
import "os"
import "strings"
import "github.com/evanw/esbuild/pkg/api"
var envPlugin = api.Plugin{
Name: "env",
Setup: func(build api.PluginBuild) {
// Intercept import paths called "env" so esbuild doesn't attempt
// to map them to a file system location. Tag them with the "env-ns"
// namespace to reserve them for this plugin.
build.OnResolve(api.OnResolveOptions{Filter: `^env$`},
func(args api.OnResolveArgs) (api.OnResolveResult, error) {
return api.OnResolveResult{
Path: args.Path,
Namespace: "env-ns",
}, nil
})
// Load paths tagged with the "env-ns" namespace and behave as if
// they point to a JSON file containing the environment variables.
build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: "env-ns"},
func(args api.OnLoadArgs) (api.OnLoadResult, error) {
mappings := make(map[string]string)
for _, item := range os.Environ() {
if equals := strings.IndexByte(item, '='); equals != -1 {
mappings[item[:equals]] = item[equals+1:]
}
}
bytes, err := json.Marshal(mappings)
if err != nil {
return api.OnLoadResult{}, err
}
contents := string(bytes)
return api.OnLoadResult{
Contents: &contents,
Loader: api.LoaderJSON,
}, nil
})
},
}
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Outfile: "out.js",
Plugins: []api.Plugin{envPlugin},
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
您會像這樣使用它
import { PATH } from 'env'
console.log(`PATH is ${PATH}`)
#概念
為 esbuild 編寫外掛程式的工作方式與為其他套件管理工具編寫外掛程式略有不同。在開發外掛程式之前,了解以下概念非常重要
#命名空間
每個模組都有關聯的命名空間。預設情況下,esbuild 在 file
命名空間中運作,這對應於檔案系統上的檔案。但是,esbuild 也可以處理沒有對應檔案系統位置的「虛擬」模組。發生這種情況的一個例子是當使用 stdin 提供模組時。
外掛程式可用於建立虛擬模組。虛擬模組通常使用 file
以外的命名空間來區分它們與檔案系統模組。通常,命名空間是特定於建立它們的外掛程式。例如,以下範例 HTTP 外掛程式 使用 http-url
命名空間來表示已下載的檔案。
#篩選器
每個回呼都必須提供正規表示式作為篩選器。esbuild 使用它來略過在路徑與其篩選器不匹配時呼叫回呼,這是為了效能。從 esbuild 的高度並行內部呼叫到單執行緒 JavaScript 程式碼的代價很高,並且應該盡可能避免,以達到最大的速度。
您應該盡可能使用篩選器正規表示式,而不是使用 JavaScript 程式碼進行篩選。這是因為正規表示式會在 esbuild 內部評估,而完全不會呼叫 JavaScript。例如,以下範例 HTTP 外掛程式 使用 ^https?://
篩選器,以確保僅針對以 http://
或 https://
開頭的路徑產生執行外掛程式的效能開銷。
允許的正規表示式語法是 Go 的 正規表示式引擎 所支援的語法。這與 JavaScript 略有不同。具體來說,不支援前瞻、後顧和反向參照。Go 的正規表示式引擎旨在避免會影響 JavaScript 正規表示式的災難性指數時間最差情況效能問題。
請注意,命名空間也可以用於篩選。回呼必須提供篩選器正規表示式,但也可以選擇性地提供命名空間以進一步限制匹配的路徑。這對於「記住」虛擬模組的來源可能很有用。請記住,命名空間是使用精確的字串相等性測試進行匹配,而不是正規表示式,因此與模組路徑不同,它們不用於儲存任意資料。
#on-resolve 回呼
使用 onResolve
新增的回呼函式會在 esbuild 建置的每個模組的每個匯入路徑上執行。回呼函式可以自訂 esbuild 執行路徑解析的方式。例如,它可以攔截匯入路徑並將其重新導向到其他地方。它也可以將路徑標記為外部路徑。以下是範例
import * as esbuild from 'esbuild'
import path from 'node:path'
let exampleOnResolvePlugin = {
name: 'example',
setup(build) {
// Redirect all paths starting with "images/" to "./public/images/"
build.onResolve({ filter: /^images\// }, args => {
return { path: path.join(args.resolveDir, 'public', args.path) }
})
// Mark all paths starting with "http://" or "https://" as external
build.onResolve({ filter: /^https?:\/\// }, args => {
return { path: args.path, external: true }
})
},
}
await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
outfile: 'out.js',
plugins: [exampleOnResolvePlugin],
loader: { '.png': 'binary' },
})
package main
import "os"
import "path/filepath"
import "github.com/evanw/esbuild/pkg/api"
var exampleOnResolvePlugin = api.Plugin{
Name: "example",
Setup: func(build api.PluginBuild) {
// Redirect all paths starting with "images/" to "./public/images/"
build.OnResolve(api.OnResolveOptions{Filter: `^images/`},
func(args api.OnResolveArgs) (api.OnResolveResult, error) {
return api.OnResolveResult{
Path: filepath.Join(args.ResolveDir, "public", args.Path),
}, nil
})
// Mark all paths starting with "http://" or "https://" as external
build.OnResolve(api.OnResolveOptions{Filter: `^https?://`},
func(args api.OnResolveArgs) (api.OnResolveResult, error) {
return api.OnResolveResult{
Path: args.Path,
External: true,
}, nil
})
},
}
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Outfile: "out.js",
Plugins: []api.Plugin{exampleOnResolvePlugin},
Write: true,
Loader: map[string]api.Loader{
".png": api.LoaderBinary,
},
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
回呼函式可以不提供路徑,將路徑解析的責任傳遞給下一個回呼函式。對於指定的匯入路徑,所有外掛程式的所有 onResolve
回呼函式會按照註冊順序執行,直到其中一個承擔路徑解析的責任。如果沒有回呼函式傳回路徑,esbuild 會執行其預設的路徑解析邏輯。
請記住,許多回呼函式可能會同時執行。在 JavaScript 中,如果你的回呼函式執行昂貴的工作,例如 fs.
,且可以在另一個執行緒上執行,你應該讓回呼函式成為 async
並使用 await
(在此情況下使用 fs.
),以允許其他程式碼同時執行。在 Go 中,每個回呼函式可能會在個別的 goroutine 上執行。如果你的外掛程式使用任何共用資料結構,請務必建立適當的同步機制。
#路徑解析選項
onResolve
API 旨在在 setup
函式內呼叫,並註冊回呼函式以在特定情況下觸發。它採用幾個選項
interface OnResolveOptions {
filter: RegExp;
namespace?: string;
}
type OnResolveOptions struct {
Filter string
Namespace string
}
filter
每個回呼函式都必須提供一個 filter,即正規表示式。當路徑與此 filter 不相符時,將略過已註冊的回呼函式。你可以 在此 閱讀更多關於 filter 的資訊。
namespace
這是選用的。如果提供,回呼函式只會在提供命名空間中模組的路徑上執行。你可以 在此 閱讀更多關於命名空間的資訊。
#路徑解析引數
當 esbuild 呼叫由 onResolve
註冊的回呼函式時,它會提供這些引數,其中包含關於匯入路徑的資訊
interface OnResolveArgs {
path: string;
importer: string;
namespace: string;
resolveDir: string;
kind: ResolveKind;
pluginData: any;
}
type ResolveKind =
| 'entry-point'
| 'import-statement'
| 'require-call'
| 'dynamic-import'
| 'require-resolve'
| 'import-rule'
| 'composes-from'
| 'url-token'
type OnResolveArgs struct {
Path string
Importer string
Namespace string
ResolveDir string
Kind ResolveKind
PluginData interface{}
}
const (
ResolveEntryPoint ResolveKind
ResolveJSImportStatement ResolveKind
ResolveJSRequireCall ResolveKind
ResolveJSDynamicImport ResolveKind
ResolveJSRequireResolve ResolveKind
ResolveCSSImportRule ResolveKind
ResolveCSSComposesFrom ResolveKind
ResolveCSSURLToken ResolveKind
)
path
這是來自基礎模組原始碼的逐字未解析路徑。它可以採用任何形式。雖然 esbuild 的預設行為是將匯入路徑解釋為相對路徑或套件名稱,但外掛程式可用於引入新的路徑形式。例如,以下範例 HTTP 外掛程式 對以
http://
開頭的路徑賦予特殊意義。importer
這是包含要解析的匯入的模組路徑。請注意,此路徑只有在命名空間為
file
時才保證是檔案系統路徑。如果你想解析相對於包含匯入模組的目錄的路徑,你應該使用resolveDir
,因為它也適用於虛擬模組。namespace
這是包含要解析的匯入的模組的命名空間,由載入此檔案的 載入回呼 所設定。對於使用 esbuild 預設行為載入的模組,預設為
file
命名空間。您可以在 這裡 閱讀更多關於命名空間的資訊。resolveDir
這是解析匯入路徑到檔案系統上真實路徑時要使用的檔案系統目錄。對於
file
命名空間中的模組,此值預設為模組路徑的目錄部分。對於虛擬模組,此值預設為空,但 載入回呼 可以選擇性地也給予虛擬模組一個解析目錄。如果發生這種情況,它將提供給該檔案中未解析路徑的解析回呼。kind
這表示要解析的路徑如何被匯入。例如,
'entry-
表示路徑已提供給 API 作為進入點路徑,point' 'import-
表示路徑來自 JavaScriptstatement' import
或export
陳述式,而'import-
表示路徑來自 CSSrule' @import
規則。pluginData
此屬性從前一個外掛程式傳遞,由載入此檔案的 載入回呼 所設定。
#解析結果
這是使用 onResolve
新增的回呼可以傳回的物件,以提供自訂路徑解析。如果您想從回呼傳回而不提供路徑,只需傳回預設值(因此在 JavaScript 中為 undefined
,在 Go 中為 OnResolveResult{}
)。以下是可傳回的選用屬性
interface OnResolveResult {
errors?: Message[];
external?: boolean;
namespace?: string;
path?: string;
pluginData?: any;
pluginName?: string;
sideEffects?: boolean;
suffix?: string;
warnings?: Message[];
watchDirs?: string[];
watchFiles?: string[];
}
interface Message {
text: string;
location: Location | null;
detail: any; // The original error from a JavaScript plugin, if applicable
}
interface Location {
file: string;
namespace: string;
line: number; // 1-based
column: number; // 0-based, in bytes
length: number; // in bytes
lineText: string;
}
type OnResolveResult struct {
Errors []Message
External bool
Namespace string
Path string
PluginData interface{}
PluginName string
SideEffects SideEffects
Suffix string
Warnings []Message
WatchDirs []string
WatchFiles []string
}
type Message struct {
Text string
Location *Location
Detail interface{} // The original error from a Go plugin, if applicable
}
type Location struct {
File string
Namespace string
Line int // 1-based
Column int // 0-based, in bytes
Length int // in bytes
LineText string
}
path
將此設定為非空字串以將匯入解析到特定路徑。如果設定此項,將不會再針對此模組中的此匯入路徑執行更多解析回呼。如果未設定此項,esbuild 將繼續執行在目前回呼之後註冊的解析回呼。然後,如果路徑仍未解析,esbuild 將預設解析相對於目前模組的解析目錄的路徑。
external
將此設定為
true
以將模組標記為 外部,表示它不會包含在套件中,而是在執行時匯入。namespace
這是與解析路徑關聯的命名空間。如果留空,它將預設為非外部路徑的
file
命名空間。檔案命名空間中的路徑必須是目前檔案系統的絕對路徑(因此在 Unix 上以正斜線開頭,在 Windows 上以磁碟機代號開頭)。如果您想解析到非檔案系統路徑的路徑,您應該將命名空間設定為
file
或空字串以外的內容。這會告訴 esbuild 不要將路徑視為指向檔案系統上的某個項目。errors
和warnings
這些屬性讓您可以傳遞路徑解析期間產生的任何日誌訊息給 esbuild,這些訊息會根據目前的 日誌層級 顯示在終端機中,並出現在最終的建置結果中。例如,如果您正在呼叫函式庫,而該函式庫可能會傳回錯誤和/或警告,您會希望使用這些屬性將它們轉發出去。
如果您只有一個錯誤要傳回,您不必透過
errors
傳遞它。您可以在 JavaScript 中直接擲回錯誤,或在 Go 中將error
物件作為第二個傳回值傳回。watchFiles
和watchDirs
這些屬性讓您可以傳回額外的檔案系統路徑,供 esbuild 的 監控模式 掃描。預設情況下,esbuild 只會掃描提供給
onLoad
外掛程式的路徑,而且只有在命名空間為file
的情況下才會掃描。如果您的外掛程式需要對檔案系統中的其他變更做出反應,它需要使用其中一個屬性。如果
watchFiles
陣列中的任何檔案自上次建置後已變更,則會觸發重新建置。變更偵測有點複雜,可能會檢查檔案內容和/或檔案的元資料。如果
watchDirs
陣列中任何目錄的目錄條目清單自上次建置後已變更,則也會觸發重新建置。請注意,這不會檢查這些目錄中任何檔案的內容,也不會檢查任何子目錄。可以將這視為檢查 Unixls
命令的輸出。為了穩健性,您應該包含在評估外掛程式期間使用的所有檔案系統路徑。例如,如果您的外掛程式執行等同於
require.resolve()
的操作,您需要包含所有「此檔案是否存在」檢查的路徑,而不仅仅是最終路徑。否則,可能會建立一個新的檔案,導致建置過時,但 esbuild 沒有偵測到,因為該路徑未列出。pluginName
這個屬性讓您可以使用另一個名稱取代此外掛程式的名稱,以進行此路徑解析操作。這對於透過此外掛程式代理另一個外掛程式很有用。例如,它讓您可以擁有單一外掛程式,轉發到包含多個外掛程式的子處理程序。您可能不需要使用這個。
pluginData
此屬性會傳遞給外掛程式鏈中執行的下一個外掛程式。如果您從
onLoad
外掛程式傳回它,它會傳遞給該檔案中任何匯入的onResolve
外掛程式,如果您從onResolve
外掛程式傳回它,當它載入檔案時,會將任意一個傳遞給onLoad
外掛程式(它是任意的,因為關係是多對一)。這對於在不同的外掛程式之間傳遞資料很有用,而無需它們直接協調。副作用
將此屬性設定為 false 會告知 esbuild,如果未使用的輸入名稱,則可以移除此模組的輸入。這會像是在對應的
package.json
檔案中指定"sideEffects": false
一樣。例如,如果x
未使用,且y
已標記為sideEffects: false
,則可能會完全移除import { x }
。您可以在 Webpack 的功能說明文件 中進一步了解from "y" sideEffects
的意義。後綴
在此處傳回值可讓您傳遞一個選用的 URL 查詢或雜湊,以附加到未包含在路徑本身的路徑。當路徑由不了解後綴的某個項目處理時,將此項目分開儲存會很有用,無論是由 esbuild 本身或其他外掛程式處理。
例如,一個 on-resolve 外掛程式可能會傳回
.eot
檔案的?#iefix
後綴,在一個針對以.eot
結尾的路徑有不同 on-load 外掛程式的組建中。保持後綴分開表示後綴仍與路徑相關聯,但.eot
外掛程式仍會比對檔案,而無需了解任何有關後綴的資訊。如果您設定後綴,它必須以
?
或#
開頭,因為它預計是 URL 查詢或雜湊。此功能有一些模糊的用途,例如破解 IE8 CSS 解析器中的錯誤,否則可能不會那麼有用。如果您使用它,請記住 esbuild 將每個唯一的命名空間、路徑和後綴組合視為唯一的模組識別碼,因此透過傳回相同路徑的不同後綴,您會告知 esbuild 建立模組的另一個副本。
#載入時回呼
使用 onLoad
新增的回呼將針對每個未標記為外部的唯一路徑/命名空間配對執行。它的工作是傳回模組的內容,並告知 esbuild 如何解譯它。以下是一個範例外掛程式,它將 .txt
檔案轉換為一個字詞陣列
import * as esbuild from 'esbuild'
import fs from 'node:fs'
let exampleOnLoadPlugin = {
name: 'example',
setup(build) {
// Load ".txt" files and return an array of words
build.onLoad({ filter: /\.txt$/ }, async (args) => {
let text = await fs.promises.readFile(args.path, 'utf8')
return {
contents: JSON.stringify(text.split(/\s+/)),
loader: 'json',
}
})
},
}
await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
outfile: 'out.js',
plugins: [exampleOnLoadPlugin],
})
package main
import "encoding/json"
import "io/ioutil"
import "os"
import "strings"
import "github.com/evanw/esbuild/pkg/api"
var exampleOnLoadPlugin = api.Plugin{
Name: "example",
Setup: func(build api.PluginBuild) {
// Load ".txt" files and return an array of words
build.OnLoad(api.OnLoadOptions{Filter: `\.txt$`},
func(args api.OnLoadArgs) (api.OnLoadResult, error) {
text, err := ioutil.ReadFile(args.Path)
if err != nil {
return api.OnLoadResult{}, err
}
bytes, err := json.Marshal(strings.Fields(string(text)))
if err != nil {
return api.OnLoadResult{}, err
}
contents := string(bytes)
return api.OnLoadResult{
Contents: &contents,
Loader: api.LoaderJSON,
}, nil
})
},
}
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Outfile: "out.js",
Plugins: []api.Plugin{exampleOnLoadPlugin},
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
回呼可以在不提供模組內容的情況下傳回。在這種情況下,載入模組的責任會傳遞給下一個已註冊的回呼。對於給定的模組,所有外掛程式的 onLoad
回呼將按照註冊順序執行,直到有一個負責載入模組。如果沒有回呼傳回模組的內容,esbuild 將執行其預設的模組載入邏輯。
請記住,許多回呼可能會同時執行。在 JavaScript 中,如果您的回呼執行昂貴的工作,可以在另一個執行緒上執行,例如 fs.
,您應該讓回呼成為 async
並使用 await
(在本例中使用 fs.
)以允許其他程式碼同時執行。在 Go 中,每個回呼都可以在一個獨立的 goroutine 上執行。如果您外掛程式使用任何共用資料結構,請確保您有適當的同步機制。
#載入選項
onLoad
API 旨在在 setup
函式中呼叫,並註冊一個回呼,以便在特定情況下觸發。它需要一些選項
interface OnLoadOptions {
filter: RegExp;
namespace?: string;
}
type OnLoadOptions struct {
Filter string
Namespace string
}
filter
每個回呼函式都必須提供一個 filter,即正規表示式。當路徑與此 filter 不相符時,將略過已註冊的回呼函式。你可以 在此 閱讀更多關於 filter 的資訊。
namespace
這是選用的。如果提供,回呼函式只會在提供命名空間中模組的路徑上執行。你可以 在此 閱讀更多關於命名空間的資訊。
#載入參數
當 esbuild 呼叫由 onLoad
註冊的回呼時,它將提供這些參數,其中包含要載入模組的資訊
interface OnLoadArgs {
path: string;
namespace: string;
suffix: string;
pluginData: any;
with: Record<string, string>;
}
type OnLoadArgs struct {
Path string
Namespace string
Suffix string
PluginData interface{}
With map[string]string
}
path
這是模組的完整解析路徑。如果命名空間是
file
,則應視為檔案系統路徑,但否則路徑可以採用任何形式。例如,以下 HTTP 外掛程式範例賦予以http://
開頭的路徑特殊意義。namespace
這是模組路徑所在的命名空間,由解析此檔案的 on-resolve 回呼 設定。對於使用 esbuild 預設行為載入的模組,其預設為
file
命名空間。您可以 在此處進一步了解命名空間。後綴
這是檔案路徑末尾的 URL 查詢和/或雜湊(如果有的話)。它是由 esbuild 的原生路徑解析行為填入,或由解析此檔案的 on-resolve 回呼 傳回。這與路徑分開儲存,以便大多數外掛程式可以只處理路徑並忽略字尾。內建於 esbuild 的載入行為只會忽略字尾,並從其路徑載入檔案。
根據背景,IE8 的 CSS 解析器有一個錯誤,它會將某些 URL 視為延伸到最後一個
)
,而不是第一個)
。因此,CSS 程式碼url('Foo.eot')
被錯誤地視為 URL 為format('eot') Foo.eot')
。為了避免這種情況,人們通常會新增類似format('eot ?#iefix
的內容,以便 IE8 將 URL 視為Foo.eot?#iefix')
。然後,URL 的路徑部分為format('eot Foo.eot
,而查詢部分為?#iefix')
,這表示 IE8 可以透過捨棄查詢來找到檔案format('eot Foo.eot
。esbuild 新增了後綴功能,用於處理包含這些 hack 的 CSS 檔案。如果所有符合
*.eot
的檔案都已標記為外部,則Foo.eot?#iefix
的 URL 應視為 外部,但最終輸出檔案中仍應保留?#iefix
後綴。pluginData
此屬性從前一個外掛程式傳遞,由外掛程式鏈中執行的 on-resolve 回呼函式 設定。
with
此屬性包含用於匯入此模組的匯入陳述式上存在的 匯入屬性 的對應。例如,使用
with {
匯入的模組會提供type: 'json' } { type:
的'json' } with
值給外掛程式。每個獨特的匯入屬性組合都會個別載入指定的模組,因此這些屬性保證由所有用於匯入此模組的匯入陳述式提供。這表示外掛程式可以使用這些屬性來變更此模組的內容。
#載入結果
這是使用 onLoad
新增的回呼函式可以傳回的物件,用於提供模組的內容。如果您想從回呼函式傳回而不提供任何內容,只需傳回預設值(在 JavaScript 中為 undefined
,在 Go 中為 OnLoadResult{}
)。以下是可傳回的選用屬性
interface OnLoadResult {
contents?: string | Uint8Array;
errors?: Message[];
loader?: Loader;
pluginData?: any;
pluginName?: string;
resolveDir?: string;
warnings?: Message[];
watchDirs?: string[];
watchFiles?: string[];
}
interface Message {
text: string;
location: Location | null;
detail: any; // The original error from a JavaScript plugin, if applicable
}
interface Location {
file: string;
namespace: string;
line: number; // 1-based
column: number; // 0-based, in bytes
length: number; // in bytes
lineText: string;
}
type OnLoadResult struct {
Contents *string
Errors []Message
Loader Loader
PluginData interface{}
PluginName string
ResolveDir string
Warnings []Message
WatchDirs []string
WatchFiles []string
}
type Message struct {
Text string
Location *Location
Detail interface{} // The original error from a Go plugin, if applicable
}
type Location struct {
File string
Namespace string
Line int // 1-based
Column int // 0-based, in bytes
Length int // in bytes
LineText string
}
contents
將此屬性設定為字串以指定模組的內容。如果設定此屬性,將不會再為此已解析路徑執行更多載入回呼函式。如果未設定此屬性,esbuild 將繼續執行在目前回呼函式之後註冊的載入回呼函式。然後,如果仍未設定內容,如果已解析路徑位於
file
名稱空間中,esbuild 將預設從檔案系統載入內容。loader
此屬性會告訴 esbuild 如何詮釋內容。例如,
js
載入器會將內容詮釋為 JavaScript,而css
載入器會將內容詮釋為 CSS。如果未指定載入器,預設為js
。請參閱 內容類型 頁面,以取得所有內建載入器的完整清單。resolveDir
在這個模組中將匯入路徑解析為檔案系統上的真實路徑時,這是要使用的檔案系統目錄。對於
file
名稱空間中的模組,此值預設為模組路徑的目錄部分。否則,此值預設為空,除非外掛程式提供一個值。如果外掛程式沒有提供值,esbuild 的預設行為不會解析此模組中的任何匯入。此目錄將傳遞給在這個模組中未解析的匯入路徑上執行的任何 on-resolve 回呼函式。errors
和warnings
這些屬性讓您可以傳遞路徑解析期間產生的任何日誌訊息給 esbuild,這些訊息會根據目前的 日誌層級 顯示在終端機中,並出現在最終的建置結果中。例如,如果您正在呼叫函式庫,而該函式庫可能會傳回錯誤和/或警告,您會希望使用這些屬性將它們轉發出去。
如果您只有一個錯誤要傳回,您不必透過
errors
傳遞它。您可以在 JavaScript 中直接擲回錯誤,或在 Go 中將error
物件作為第二個傳回值傳回。watchFiles
和watchDirs
這些屬性讓您可以傳回額外的檔案系統路徑,供 esbuild 的 監控模式 掃描。預設情況下,esbuild 只會掃描提供給
onLoad
外掛程式的路徑,而且只有在命名空間為file
的情況下才會掃描。如果您的外掛程式需要對檔案系統中的其他變更做出反應,它需要使用其中一個屬性。如果
watchFiles
陣列中的任何檔案自上次建置後已變更,則會觸發重新建置。變更偵測有點複雜,可能會檢查檔案內容和/或檔案的元資料。如果
watchDirs
陣列中任何目錄的目錄條目清單自上次建置後已變更,則也會觸發重新建置。請注意,這不會檢查這些目錄中任何檔案的內容,也不會檢查任何子目錄。可以將這視為檢查 Unixls
命令的輸出。為了穩健性,您應該包含在評估外掛程式期間使用的所有檔案系統路徑。例如,如果您的外掛程式執行等同於
require.resolve()
的操作,您需要包含所有「此檔案是否存在」檢查的路徑,而不仅仅是最終路徑。否則,可能會建立一個新的檔案,導致建置過時,但 esbuild 沒有偵測到,因為該路徑未列出。pluginName
此屬性讓您可以使用另一個名稱取代此外掛程式的名稱,以進行此模組載入作業。這對於透過此外掛程式代理另一個外掛程式很有用。例如,它讓您可以擁有單一外掛程式,轉發至包含多個外掛程式的子處理程序。您可能不需要使用此功能。
pluginData
此屬性會傳遞給外掛程式鏈中執行的下一個外掛程式。如果您從
onLoad
外掛程式傳回它,它會傳遞給該檔案中任何匯入的onResolve
外掛程式,如果您從onResolve
外掛程式傳回它,當它載入檔案時,會將任意一個傳遞給onLoad
外掛程式(它是任意的,因為關係是多對一)。這對於在不同的外掛程式之間傳遞資料很有用,而無需它們直接協調。
#快取您的外掛程式
由於 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
})
}
}
關於上述快取程式碼的一些重要注意事項
上述程式碼中沒有快取驅逐原則。如果將越來越多的鍵新增到快取對應,記憶體使用量將持續增加。
input
值儲存在快取value
中,而不是快取key
中。這表示變更檔案內容不會洩漏記憶體,因為 key 只包含檔案路徑,而不包含檔案內容。變更檔案內容只會覆寫先前的快取項目。這對於常見的使用方式而言可能沒問題,例如有人在增量重新建置期間重複編輯同一個檔案,而且只會偶爾新增或重新命名檔案。快取失效只有在
slowTransform()
是 純函數(表示函數的輸出只取決於函數的輸入)時才有效,而且函數的所有輸入都以某種方式擷取到快取對應中。例如,如果轉換函數自動讀取其他一些檔案的內容,而且輸出也取決於這些檔案的內容,那麼當這些檔案變更時,快取將無法失效,因為這些檔案未包含在快取 key 中。這個部分很容易搞錯,所以值得仔細看一個具體的範例。考慮一個實作編譯成 CSS 語言的外掛程式。如果該外掛程式透過剖析匯入的檔案並將它們捆綁在一起或讓匯入程式碼可以使用任何匯出的變數宣告,自行實作
@import
規則,如果外掛程式只檢查匯入檔案的內容沒有變更,那麼外掛程式就不正確,因為匯入檔案的變更也可能會使快取失效。你可能會想,你可以將匯入檔案的內容新增到快取 key 中來解決這個問題。但是,即使這樣也可能不正確。例如,假設這個外掛程式使用
require.resolve()
將匯入路徑解析成絕對檔案路徑。這是一個常見的做法,因為它使用 Node 內建的路徑解析,可以解析成套件中的路徑。這個函數通常會在傳回解析的路徑之前,檢查不同位置的檔案。例如,從檔案src/entry.css
匯入路徑pkg/file
可能會檢查下列位置(是的,Node 的套件解析演算法非常沒有效率)src/node_modules/pkg/file src/node_modules/pkg/file.css src/node_modules/pkg/file/package.json src/node_modules/pkg/file/main src/node_modules/pkg/file/main.css src/node_modules/pkg/file/main/index.css src/node_modules/pkg/file/index.css node_modules/pkg/file node_modules/pkg/file.css node_modules/pkg/file/package.json node_modules/pkg/file/main node_modules/pkg/file/main.css node_modules/pkg/file/main/index.css node_modules/pkg/file/index.css
假設匯入
pkg/file
最終解析成絕對路徑node_modules/
。即使你快取匯入檔案和匯入檔案的內容,而且在重新使用快取項目之前驗證兩個檔案的內容仍然相同,如果pkg/ file/ index.css require.resolve()
檢查的其他檔案之一在新增快取項目之後已被建立或刪除,快取項目仍然可能過期。正確快取這項內容基本上包括在輸入檔案都沒有變更時,總是重新執行所有這些路徑解析,而且驗證沒有任何路徑解析已變更。這些快取 key 只適用於記憶體中的快取。使用相同的快取 key 來實作檔案系統快取是不正確的。雖然記憶體中的快取保證會在每次建置時執行相同的程式碼,因為程式碼也儲存在記憶體中,但檔案系統快取可能會被兩個不同的建置存取,而每個建置都包含不同的程式碼。特別是
slowTransform()
函數的程式碼可能在建置期間已變更。這可能會在各種情況下發生。包含函式 `slowTransform()` 的套件可能已更新,或者其傳遞依賴項之一可能已更新,即使您已固定套件版本,這是由於 npm 處理 semver 的方式,或者有人可能已 變更套件內容 在這段期間的檔案系統中,或者轉換函式可能正在呼叫節點 API,而不同的建置可能會在不同的節點版本上執行。
如果您想將快取儲存在檔案系統中,您應該防範轉換函式的程式碼變更,方法是在快取金鑰中儲存轉換函式的程式碼的一些表示形式。這通常是某種形式的 雜湊,其中包含所有相關套件中所有相關檔案的內容,以及其他詳細資訊,例如您目前執行的節點版本。要讓所有這些都正確並不容易。
#啟動時回呼
註冊啟動時回呼,以便在新的建置開始時收到通知。這會觸發所有建置,而不僅僅是初始建置,因此對於 重建、監看模式 和 服務模式 特別有用。以下是新增啟動時回呼的方法
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 回呼的方法
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 回呼的方法
let examplePlugin = {
name: 'example',
setup(build) {
build.onDispose(() => {
console.log('This plugin is no longer used')
})
},
}
package main
import "fmt"
import "github.com/evanw/esbuild/pkg/api"
var examplePlugin = api.Plugin{
Name: "example",
Setup: func(build api.PluginBuild) {
build.OnDispose(func() {
fmt.Println("This plugin is no longer used")
})
},
}
func main() {
}
#存取建置選項
外掛程式可以在 setup
方法中存取初始建置選項。這讓您可以檢查建置如何設定,以及在建置開始前修改建置選項。以下是範例
let examplePlugin = {
name: 'auto-node-env',
setup(build) {
const options = build.initialOptions
options.define = options.define || {}
options.define['process.env.NODE_ENV'] =
options.minify ? '"production"' : '"development"'
},
}
package main
import "github.com/evanw/esbuild/pkg/api"
var examplePlugin = api.Plugin{
Name: "auto-node-env",
Setup: func(build api.PluginBuild) {
options := build.InitialOptions
if options.Define == nil {
options.Define = map[string]string{}
}
if options.MinifyWhitespace && options.MinifyIdentifiers && options.MinifySyntax {
options.Define[`process.env.NODE_ENV`] = `"production"`
} else {
options.Define[`process.env.NODE_ENV`] = `"development"`
}
},
}
func main() {
}
請注意,在建置開始後修改建置選項不會影響建置。特別是,重建、監控模式 和 提供服務模式 如果你在外掛程式於第一次建置開始後變更建置選項物件,它們不會更新其建置選項。
#解析路徑
當外掛程式從 on-resolve 回呼 回傳結果時,結果會完全取代 esbuild 內建的路徑解析。這讓外掛程式完全控制路徑解析的運作方式,但這表示如果外掛程式想要有類似的行為,它可能必須重新實作 esbuild 已經內建的一些行為。例如,外掛程式可能想要在使用者的 node_modules
目錄中搜尋套件,這是 esbuild 已經實作的功能。
外掛程式可以選擇手動執行 esbuild 的路徑解析並檢查結果,而不是重新實作 esbuild 的內建行為。這讓您可以調整 esbuild 路徑解析的輸入和/或輸出。以下是範例
import * as esbuild from 'esbuild'
let examplePlugin = {
name: 'example',
setup(build) {
build.onResolve({ filter: /^example$/ }, async () => {
const result = await build.resolve('./foo', {
kind: 'import-statement',
resolveDir: './bar',
})
if (result.errors.length > 0) {
return { errors: result.errors }
}
return { path: result.path, external: true }
})
},
}
await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
outfile: 'out.js',
plugins: [examplePlugin],
})
package main
import "os"
import "github.com/evanw/esbuild/pkg/api"
var examplePlugin = api.Plugin{
Name: "example",
Setup: func(build api.PluginBuild) {
build.OnResolve(api.OnResolveOptions{Filter: `^example$`},
func(api.OnResolveArgs) (api.OnResolveResult, error) {
result := build.Resolve("./foo", api.ResolveOptions{
Kind: api.ResolveJSImportStatement,
ResolveDir: "./bar",
})
if len(result.Errors) > 0 {
return api.OnResolveResult{Errors: result.Errors}, nil
}
return api.OnResolveResult{Path: result.Path, External: true}, nil
})
},
}
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Outfile: "out.js",
Plugins: []api.Plugin{examplePlugin},
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
這個外掛程式攔截對路徑 example
的匯入,告訴 esbuild 在目錄 ./bar
中解析匯入 ./foo
,強制 esbuild 回傳的任何路徑都被視為外部,並將 example
的匯入對應到該外部路徑。
以下是關於此 API 的一些其他事項
如果你沒有傳遞可選的
resolveDir
參數,esbuild 仍會執行onResolve
外掛程式回呼,但不會嘗試自行解析任何路徑。esbuild 的所有路徑解析邏輯都依賴於resolveDir
參數,包括在node_modules
目錄中尋找套件(因為它需要知道這些node_modules
目錄可能在哪裡)。如果你想解析特定目錄中的檔案名稱,請確保輸入路徑以
./
開頭。否則,輸入路徑將被視為套件路徑,而不是相對路徑。此行為與 esbuild 的正常路徑解析邏輯相同。如果路徑解析失敗,則傳回物件的
errors
屬性將會是一個包含錯誤資訊的非空陣列。此函式在失敗時並不總是會擲回錯誤。你必須在呼叫它之後檢查錯誤。此函式的行為取決於建置設定。這就是為什麼它是
build
物件的屬性,而不是頂層 API 呼叫。這也表示你無法在所有外掛程式setup
函式完成之前呼叫它,因為這些函式讓外掛程式有機會在建置開始時凍結建置設定之前調整建置設定。因此,resolve
函式在你的onResolve
和/或onLoad
回呼中將最為有用。目前並未嘗試偵測無限路徑解析迴圈。在
onResolve
中使用相同的參數從resolve
呼叫幾乎肯定是一個壞主意。
#解析選項
resolve
函式將要解析的路徑作為第一個引數,並將一個具有可選屬性的物件作為第二個引數。此選項物件與 傳遞給 onResolve
的引數 非常相似。以下是可用的選項
interface ResolveOptions {
kind: ResolveKind;
importer?: string;
namespace?: string;
resolveDir?: string;
pluginData?: any;
}
type ResolveKind =
| 'entry-point'
| 'import-statement'
| 'require-call'
| 'dynamic-import'
| 'require-resolve'
| 'import-rule'
| 'url-token'
type ResolveOptions struct {
Kind ResolveKind
Importer string
Namespace string
ResolveDir string
PluginData interface{}
}
const (
ResolveEntryPoint ResolveKind
ResolveJSImportStatement ResolveKind
ResolveJSRequireCall ResolveKind
ResolveJSDynamicImport ResolveKind
ResolveJSRequireResolve ResolveKind
ResolveCSSImportRule ResolveKind
ResolveCSSURLToken ResolveKind
)
kind
這告訴 esbuild 路徑是如何匯入的,這可能會影響路徑解析。例如,Node 的路徑解析規則 指出使用
'require-call'
匯入的路徑應遵守package.json
中"require"
區段中的 條件套件匯入,而使用'import-statement'
匯入的路徑應遵守"import"
區段中的條件套件匯入。importer
如果已設定,則將此解釋為要解析此匯入的模組路徑。這會影響具有
onResolve
回呼並檢查importer
值的外掛程式。namespace
如果已設定,則將此解釋為要解析此匯入的模組命名空間。這會影響具有
onResolve
回呼並檢查namespace
值的外掛程式。你可以在 這裡 閱讀更多關於命名空間的資訊。resolveDir
這是用於將匯入路徑解析為檔案系統上的真實路徑時使用的檔案系統目錄。必須設定此項才能讓 esbuild 內建的路徑解析找到給定的檔案,即使是非相對套件路徑也是如此(因為 esbuild 需要知道
node_modules
目錄在哪裡)。pluginData
此屬性可用於將自訂資料傳遞給與此匯入路徑相符的任何 on-resolve 回呼。此資料的意義完全取決於你。
#解析結果
resolve
函式傳回一個物件,這個物件與外掛程式可以 從 onResolve
回呼傳回的物件 非常相似。它具有下列屬性
export interface ResolveResult {
errors: Message[];
external: boolean;
namespace: string;
path: string;
pluginData: any;
sideEffects: boolean;
suffix: string;
warnings: Message[];
}
interface Message {
text: string;
location: Location | null;
detail: any; // The original error from a JavaScript plugin, if applicable
}
interface Location {
file: string;
namespace: string;
line: number; // 1-based
column: number; // 0-based, in bytes
length: number; // in bytes
lineText: string;
}
type ResolveResult struct {
Errors []Message
External bool
Namespace string
Path string
PluginData interface{}
SideEffects bool
Suffix string
Warnings []Message
}
type Message struct {
Text string
Location *Location
Detail interface{} // The original error from a Go plugin, if applicable
}
type Location struct {
File string
Namespace string
Line int // 1-based
Column int // 0-based, in bytes
Length int // in bytes
LineText string
}
path
這是路徑解析的結果,或如果路徑解析失敗,則為空字串。
external
如果路徑標記為外部,則這將為
true
,這表示它不會包含在套件中,而是在執行時匯入。namespace
這是與解析路徑關聯的命名空間。您可以在這裡閱讀更多有關命名空間的資訊。
errors
和warnings
這些屬性包含路徑解析期間產生的任何記錄訊息,這些訊息可能是回應此路徑解析操作的任何外掛程式或 esbuild 本身產生的。這些記錄訊息不會自動包含在記錄中,因此如果您捨棄它們,它們將完全不可見。如果您希望將它們包含在記錄中,您需要從
onResolve
或onLoad
傳回它們。pluginData
如果外掛程式回應此路徑解析操作並從其
onResolve
回呼傳回pluginData
,該資料將出現在這裡。這對於在不同的外掛程式之間傳遞資料很有用,而無需它們直接協調。副作用
此屬性將為
true
,除非模組以某種方式註解為沒有副作用,否則將為false
。對於在對應的package.json
檔案中具有"sideEffects": false
的套件,這將為false
,並且如果外掛程式回應此路徑解析操作並傳回sideEffects: false
,這也將為false
。您可以在Webpack 關於此功能的文件中閱讀更多有關sideEffects
的含義。後綴
如果在要解析的路徑結尾處有一個選用的 URL 查詢或雜湊,並且移除它對於路徑成功解析是必要的,則此處可以包含它。
#範例外掛程式
以下範例外掛程式旨在讓您了解您可以使用外掛程式 API 執行的不同類型的工作。
#HTTP 外掛程式
此範例展示:使用檔案系統路徑以外的路徑格式、特定於命名空間的路徑解析、同時使用解析和載入回呼。
此外掛程式允許您將 HTTP URL 匯入 JavaScript 程式碼。該程式碼將在建置時自動下載。它啟用以下工作流程
import { zip } from 'https://unpkg.com/lodash-es@4.17.15/lodash.js'
console.log(zip([1, 2], ['a', 'b']))
可以使用以下外掛程式來完成此任務。請注意,對於實際使用,應快取下載,但此範例已省略快取以簡潔起見
import * as esbuild from 'esbuild'
import https from 'node:https'
import http from 'node:http'
let httpPlugin = {
name: 'http',
setup(build) {
// Intercept import paths starting with "http:" and "https:" so
// esbuild doesn't attempt to map them to a file system location.
// Tag them with the "http-url" namespace to associate them with
// this plugin.
build.onResolve({ filter: /^https?:\/\// }, args => ({
path: args.path,
namespace: 'http-url',
}))
// We also want to intercept all import paths inside downloaded
// files and resolve them against the original URL. All of these
// files will be in the "http-url" namespace. Make sure to keep
// the newly resolved URL in the "http-url" namespace so imports
// inside it will also be resolved as URLs recursively.
build.onResolve({ filter: /.*/, namespace: 'http-url' }, args => ({
path: new URL(args.path, args.importer).toString(),
namespace: 'http-url',
}))
// When a URL is loaded, we want to actually download the content
// from the internet. This has just enough logic to be able to
// handle the example import from unpkg.com but in reality this
// would probably need to be more complex.
build.onLoad({ filter: /.*/, namespace: 'http-url' }, async (args) => {
let contents = await new Promise((resolve, reject) => {
function fetch(url) {
console.log(`Downloading: ${url}`)
let lib = url.startsWith('https') ? https : http
let req = lib.get(url, res => {
if ([301, 302, 307].includes(res.statusCode)) {
fetch(new URL(res.headers.location, url).toString())
req.abort()
} else if (res.statusCode === 200) {
let chunks = []
res.on('data', chunk => chunks.push(chunk))
res.on('end', () => resolve(Buffer.concat(chunks)))
} else {
reject(new Error(`GET ${url} failed: status ${res.statusCode}`))
}
}).on('error', reject)
}
fetch(args.path)
})
return { contents }
})
},
}
await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
outfile: 'out.js',
plugins: [httpPlugin],
})
package main
import "io/ioutil"
import "net/http"
import "net/url"
import "os"
import "github.com/evanw/esbuild/pkg/api"
var httpPlugin = api.Plugin{
Name: "http",
Setup: func(build api.PluginBuild) {
// Intercept import paths starting with "http:" and "https:" so
// esbuild doesn't attempt to map them to a file system location.
// Tag them with the "http-url" namespace to associate them with
// this plugin.
build.OnResolve(api.OnResolveOptions{Filter: `^https?://`},
func(args api.OnResolveArgs) (api.OnResolveResult, error) {
return api.OnResolveResult{
Path: args.Path,
Namespace: "http-url",
}, nil
})
// We also want to intercept all import paths inside downloaded
// files and resolve them against the original URL. All of these
// files will be in the "http-url" namespace. Make sure to keep
// the newly resolved URL in the "http-url" namespace so imports
// inside it will also be resolved as URLs recursively.
build.OnResolve(api.OnResolveOptions{Filter: ".*", Namespace: "http-url"},
func(args api.OnResolveArgs) (api.OnResolveResult, error) {
base, err := url.Parse(args.Importer)
if err != nil {
return api.OnResolveResult{}, err
}
relative, err := url.Parse(args.Path)
if err != nil {
return api.OnResolveResult{}, err
}
return api.OnResolveResult{
Path: base.ResolveReference(relative).String(),
Namespace: "http-url",
}, nil
})
// When a URL is loaded, we want to actually download the content
// from the internet. This has just enough logic to be able to
// handle the example import from unpkg.com but in reality this
// would probably need to be more complex.
build.OnLoad(api.OnLoadOptions{Filter: ".*", Namespace: "http-url"},
func(args api.OnLoadArgs) (api.OnLoadResult, error) {
res, err := http.Get(args.Path)
if err != nil {
return api.OnLoadResult{}, err
}
defer res.Body.Close()
bytes, err := ioutil.ReadAll(res.Body)
if err != nil {
return api.OnLoadResult{}, err
}
contents := string(bytes)
return api.OnLoadResult{Contents: &contents}, nil
})
},
}
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Outfile: "out.js",
Plugins: []api.Plugin{httpPlugin},
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
此外掛程式首先使用解析器將 http://
和 https://
URL 移至 http-url
命名空間。設定命名空間會告訴 esbuild 不要將這些路徑視為檔案系統路徑。然後,http-url
命名空間的載入程式會下載模組並將內容傳回 esbuild。從那裡,http-url
命名空間中模組內匯入路徑的另一個解析器會選取相對路徑,並透過根據匯入模組的 URL 解析它們,將它們轉換為完整的 URL。然後,這會回饋給載入程式,允許已下載的模組遞迴下載其他模組。
#WebAssembly 外掛程式
此範例示範:處理二進位資料、使用匯入陳述式建立虛擬模組、使用不同命名空間重新使用相同路徑。
此外掛程式讓您能將 .wasm
檔案匯入 JavaScript 程式碼。它不會產生 WebAssembly 檔案本身;這可以由其他工具完成,或修改此範例外掛程式以符合您的需求。它啟用下列工作流程
import load from './example.wasm'
load(imports).then(exports => { ... })
當您匯入 .wasm
檔案時,此外掛程式會在 wasm-stub
命名空間中產生一個虛擬 JavaScript 模組,其中包含一個載入 WebAssembly 模組的函式,該模組已匯出為預設匯出。該 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
檔案實際上會產生兩個虛擬模組。以下是外掛程式的程式碼
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 編寫的。如果外掛程式套用於建置中的每個檔案,則建置速度可能會非常慢。如果適用快取,則必須由外掛程式本身執行。