プラグイン
プラグインAPIを使用すると、ビルドプロセスのさまざまな部分にコードを挿入できます。APIの他の部分とは異なり、コマンドラインからは使用できません。プラグインAPIを使用するには、JavaScriptまたはGoコードを記述する必要があります。また、プラグインはtransform APIではなく、build APIでのみ使用できます。
#プラグインの検索
既存のesbuildプラグインを探している場合は、既存のesbuildプラグインのリストを確認してください。このリストにあるプラグインは、作者によって意図的に追加されており、esbuildコミュニティの他のユーザーが使用することを目的としています。
esbuildプラグインを共有したい場合は、
- npmに公開して、他のユーザーがインストールできるようにしてください。
- 既存のesbuildプラグインのリストに追加して、他のユーザーが見つけられるようにしてください。
#プラグインの使用
esbuildプラグインは、name
とsetup
関数を持つオブジェクトです。build API呼び出しに配列で渡されます。setup
関数は、ビルドAPI呼び出しごとに1回実行されます。
ビルド時に現在の環境変数をインポートできる簡単なプラグインの例を次に示します。
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はファイルシステムに対応する場所を持たない「仮想」モジュールも処理できます。これが発生する1つのケースは、stdinを使用してモジュールが提供される場合です。
プラグインを使用して仮想モジュールを作成できます。仮想モジュールは通常、ファイルシステムモジュールと区別するためにfile
以外の名前空間を使用します。通常、名前空間はそれらを作成したプラグインに固有です。たとえば、以下のサンプルのHTTPプラグインは、ダウンロードされたファイルにhttp-url
名前空間を使用します。
#フィルター
すべてのコールバックは、フィルターとして正規表現を提供する必要があります。これは、esbuildがパスがフィルターと一致しない場合にコールバックの呼び出しをスキップするために使用されます。これはパフォーマンスのために行われます。esbuildの高度に並列化された内部からシングルスレッドのJavaScriptコードを呼び出すことはコストがかかり、速度を最大にするために可能な限り避ける必要があります。
可能な限り、JavaScriptコードでフィルタリングする代わりに、フィルター正規表現を使用するようにしてください。正規表現はJavaScriptを呼び出さずにesbuild内で評価されるため、こちらの方が高速です。たとえば、以下のサンプルの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では、各コールバックは別のゴルーチンで実行される場合があります。プラグインが共有データ構造体を使用する場合は、適切な同期がとれていることを確認してください。
#On-resolveオプション
onResolve
APIは、setup
関数内で呼び出されることを意図しており、特定の状況でトリガーされるコールバックを登録します。いくつかのオプションがあります。
interface OnResolveOptions {
filter: RegExp;
namespace?: string;
}
type OnResolveOptions struct {
Filter string
Namespace string
}
filter
すべてのコールバックは、正規表現であるフィルターを提供する必要があります。パスがこのフィルターと一致しない場合、登録されたコールバックはスキップされます。フィルターの詳細については、こちらをご覧ください。
namespace
これはオプションです。指定した場合、コールバックは、指定された名前空間内のモジュール内のパスに対してのみ実行されます。名前空間の詳細については、こちらをご覧ください。
#On-resolve引数
esbuildがonResolve
によって登録されたコールバックを呼び出すと、インポートされたパスに関する情報を含むこれらの引数が提供されます。
interface OnResolveArgs {
path: string;
importer: string;
namespace: string;
resolveDir: string;
kind: ResolveKind;
pluginData: any;
}
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
これは、このファイルをロードしたon-loadコールバックによって設定された、解決されるこのインポートを含むモジュールの名前空間です。これは、esbuildのデフォルトの動作でロードされたモジュールの場合は、デフォルトで
file
名前空間になります。名前空間の詳細については、こちらをご覧ください。resolveDir
これは、インポートパスをファイルシステム上の実際のパスに解決するときに使用するファイルシステムディレクトリです。
file
名前空間のモジュールの場合は、この値はデフォルトでモジュールパスのディレクトリ部分になります。仮想モジュールの場合は、この値はデフォルトで空になりますが、on-loadコールバックはオプションで仮想モジュールに解決ディレクトリを与えることもできます。その場合、そのファイル内の未解決のパスに対してコールバックを解決するために提供されます。kind
これは、解決されるパスがどのようにインポートされているかを示します。たとえば、
'entry-
はパスがエントリポイントパスとしてAPIに提供されたことを意味し、point' 'import-
はパスがJavaScriptのstatement' import
またはexport
ステートメントからのものであることを意味し、'import-
はパスがCSSのrule' @import
ルールからのものであることを意味します。pluginData
このプロパティは、このファイルをロードしたon-loadコールバックによって設定された、前のプラグインから渡されます。
#On-resolve結果
これは、onResolve
を使用して追加されたコールバックによって返される可能性のあるオブジェクトであり、カスタムパス解決を提供します。パスを提供せずにコールバックから返したい場合は、デフォルト値を返すだけです(JavaScriptではundefined
、GoではOnResolveResult{}
)。返される可能性のあるオプションのプロパティは次のとおりです。
interface OnResolveResult {
errors?: Message[];
external?: boolean;
namespace?: string;
path?: string;
pluginData?: any;
pluginName?: string;
sideEffects?: boolean;
suffix?: string;
warnings?: Message[];
watchDirs?: string[];
watchFiles?: string[];
}
interface Message {
text: string;
location: Location | null;
detail: any; // The original error from a JavaScript plugin, if applicable
}
interface Location {
file: string;
namespace: string;
line: number; // 1-based
column: number; // 0-based, in bytes
length: number; // in bytes
lineText: string;
}
type OnResolveResult struct {
Errors []Message
External bool
Namespace string
Path string
PluginData interface{}
PluginName string
SideEffects SideEffects
Suffix string
Warnings []Message
WatchDirs []string
WatchFiles []string
}
type Message struct {
Text string
Location *Location
Detail interface{} // The original error from a Go plugin, if applicable
}
type Location struct {
File string
Namespace string
Line int // 1-based
Column int // 0-based, in bytes
Length int // in bytes
LineText string
}
path
path
これを空でない文字列に設定して、インポートを特定のパスに解決します。これが設定されている場合、このモジュール内のこのインポートパスに対して、これ以上on-resolveコールバックは実行されません。これが設定されていない場合、esbuildは現在のコールバックの後に登録されたon-resolveコールバックを実行し続けます。その後、パスがまだ解決されていない場合、esbuildはデフォルトで現在のモジュールの解決ディレクトリを基準にしてパスを解決します。
external
namespace
これは、解決されたパスに関連付けられた名前空間です。空のままにすると、外部パス以外の場合はデフォルトで`file`名前空間になります。`file`名前空間のパスは、現在のファイルシステムの絶対パスである必要があります(Unixではスラッシュで始まり、Windowsではドライブ文字で始まります)。
ファイルシステムパスではないパスに解決したい場合は、名前空間を`file`または空の文字列以外に設定する必要があります。これは、esbuildにパスがファイルシステム上の何かを指しているものとして扱わないように指示します。
- `errors`と`warnings`
これらのプロパティを使用すると、パス解決中に生成されたログメッセージをesbuildに渡し、現在のログレベルに従ってターミナルに表示し、最終的なビルド結果に含めることができます。たとえば、ライブラリを呼び出していて、そのライブラリがエラーや警告を返すことができる場合は、これらのプロパティを使用してそれらを転送する必要があります。
返すエラーが1つだけの場合は、`errors`を介して渡す必要はありません。JavaScriptでエラーをスローするか、Goで`error`オブジェクトを2番目の戻り値として返すことができます。
- `watchFiles`と`watchDirs`
これらのプロパティを使用すると、esbuildのウォッチモードがスキャンするための追加のファイルシステムパスを返すことができます。デフォルトでは、esbuildは`onLoad`プラグインに提供されたパスのみをスキャンし、名前空間が`file`の場合のみスキャンします。プラグインがファイルシステムの追加の変更に対応する必要がある場合は、これらのプロパティのいずれかを使用する必要があります。
`watchFiles`配列内のファイルが最後のビルド以降に変更された場合、リビルドがトリガーされます。変更の検出はやや複雑で、ファイルの内容やファイルのメタデータをチェックする場合があります。
`watchDirs`配列内のディレクトリのディレクトリエントリのリストが最後のビルド以降に変更された場合も、リビルドがトリガーされます。これは、これらのディレクトリ内のファイルの内容については何もチェックせず、サブディレクトリもチェックしないことに注意してください。これは、Unixの`ls`コマンドの出力をチェックすると考えてください。
堅牢性を高めるために、プラグインの評価中に使用されたすべてのファイルシステムパスを含める必要があります。たとえば、プラグインが`require.resolve()`と同等の処理を行う場合、最終的なパスだけでなく、すべての「このファイルは存在しますか」チェックのパスを含める必要があります。そうしないと、ビルドが古くなる原因となる新しいファイルが作成される可能性がありますが、esbuildはそのパスがリストされていないため、それを検出しません。
pluginName
このプロパティを使用すると、このプラグインの名前をこのパス解決操作の別の名前に置き換えることができます。これは、このプラグインを介して別のプラグインをプロキシする場合に便利です。たとえば、複数のプラグインを含む子プロセスに転送する単一のプラグインを持つことができます。おそらくこれを使用する必要はありません。
pluginData
このプロパティは、プラグインチェーンで実行される次のプラグインに渡されます。`onLoad`プラグインから返された場合、そのファイルのインポートに対して`onResolve`プラグインに渡され、`onResolve`プラグインから返された場合、ファイルのロード時に任意のものが`onLoad`プラグインに渡されます(関係が多対一であるため、任意です)。これは、異なるプラグインが直接調整することなく、データをやり取りするのに役立ちます。
sideEffects
このプロパティをfalseに設定すると、インポートされた名前が使用されていない場合、このモジュールのインポートを削除できることがesbuildに伝えられます。これは、対応する`package.json`ファイルで`"sideEffects": false`が指定されたかのように動作します。たとえば、`x`が使用されておらず、`y`が`sideEffects: false`としてマークされている場合、`import { x }
from "y"`は完全に削除される場合があります。`sideEffects`の意味の詳細については、Webpackの機能に関するドキュメントをご覧ください。 suffix
ここで値を返すと、パス自体には含まれていない、パスに追加するオプションのURLクエリまたはハッシュを渡すことができます。パスがesbuild自体または別のプラグインによって、サフィックスを認識しない何かによって処理される場合、これを個別に保存することは有益です。
たとえば、on-resolveプラグインは、`。eot`で終わるパスの別のon-loadプラグインを使用してビルドされた`.eot`ファイルのサフィックス`?#iefix`を返す場合があります。サフィックスを分離しておくということは、サフィックスがパスに関連付けられたままであることを意味しますが、`.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.
#オンロードオプション
`onLoad` APIは`setup`関数内で呼び出されることを意図しており、特定の状況でトリガーされるコールバックを登録します。いくつかのオプションが必要です
interface OnLoadOptions {
filter: RegExp;
namespace?: string;
}
type OnLoadOptions struct {
Filter string
Namespace string
}
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`名前空間になります。名前空間の詳細については、こちらをご覧ください。
suffix
これは、ファイルパスの末尾にあるURLクエリまたはハッシュ(存在する場合)です。esbuildのネイティブパス解決動作によって入力されるか、このファイルを解決したon-resolveコールバックによって返されます。これはパスとは別に保存されるため、ほとんどのプラグインはパスを処理してサフィックスを無視するだけで済みます。esbuildに組み込まれているオンロード動作は、サフィックスを無視し、パスだけからファイルをロードします。
コンテキストとして、IE8のCSSパーサーには、特定のURLが最初の`)`ではなく最後の`)`まで拡張されると見なすバグがあります。したがって、CSSコード`url('Foo.eot')
format('eot')`は、URLが`Foo.eot') format( 'eot`であると誤って見なされます。これを回避するために、通常は`?#iefix`のようなものを追加して、IE8がURLを`Foo.eot?#iefix') format('eot`と見なすようにします。次に、URLのパス部分は`Foo.eot`になり、クエリ部分は`?#iefix') format('eot`になります。つまり、IE8はクエリを破棄することでファイル`Foo.eot`を見つけることができます。 サフィックス機能は、これらのハックを含むCSSファイルを処理するためにesbuildに追加されました。`*。eot`に一致するすべてのファイルが外部としてマークされている場合、`Foo.eot?#iefix`のURLは外部と見なす必要がありますが、`?#iefix`サフィックスは最終出力ファイルに存在する必要があります。
pluginData
このプロパティは、プラグインチェーンで実行されるon-resolveコールバックによって設定された、前のプラグインから渡されます。
with
これには、このモジュールをインポートするために使用されたimportステートメントに存在していたインポート属性のマップが含まれています。たとえば、`with {
type: 'json' }`を使用してインポートされたモジュールは、プラグインに`{ type: 'json' }`の`with`値を提供します。指定されたモジュールは、インポート属性の一意の組み合わせごとに個別にロードされるため、これらの属性はこのモジュールをインポートするために使用されるすべてのimportステートメントによって提供されていることが保証されます。つまり、プラグインはそれらを使用してこのモジュールの内容を変更できます。
#オンロード結果
これは、`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 は現在のコールバックの後に登録されたロード時のコールバックを実行し続けます。それでも内容が設定されていない場合、esbuild は、解決されたパスが
file
名前空間に属している場合、ファイルシステムから内容を読み込むようにデフォルト設定されます。ローダー
これは、esbuild に内容の解釈方法を指示します。たとえば、
js
ローダーは内容を JavaScript として解釈し、css
ローダーは内容を CSS として解釈します。ローダーは、指定されていない場合、デフォルトでjs
になります。すべての組み込みローダーの完全なリストについては、コンテンツタイプのページを参照してください。resolveDir
これは、このモジュール内のインポートパスをファイルシステム上の実際のパスに解決するときに使用するファイルシステムディレクトリです。
file
名前空間のモジュールの場合、この値はデフォルトでモジュールパスのディレクトリ部分になります。それ以外の場合、この値は、プラグインが提供しない限り、デフォルトで空になります。プラグインが提供しない場合、esbuild のデフォルトの動作では、このモジュール内のインポートは解決されません。このディレクトリは、このモジュール内の未解決のインポートパスで実行される on-resolve コールバック に渡されます。- `errors`と`warnings`
これらのプロパティを使用すると、パス解決中に生成されたログメッセージをesbuildに渡し、現在のログレベルに従ってターミナルに表示し、最終的なビルド結果に含めることができます。たとえば、ライブラリを呼び出していて、そのライブラリがエラーや警告を返すことができる場合は、これらのプロパティを使用してそれらを転送する必要があります。
返すエラーが1つだけの場合は、`errors`を介して渡す必要はありません。JavaScriptでエラーをスローするか、Goで`error`オブジェクトを2番目の戻り値として返すことができます。
- `watchFiles`と`watchDirs`
これらのプロパティを使用すると、esbuildのウォッチモードがスキャンするための追加のファイルシステムパスを返すことができます。デフォルトでは、esbuildは`onLoad`プラグインに提供されたパスのみをスキャンし、名前空間が`file`の場合のみスキャンします。プラグインがファイルシステムの追加の変更に対応する必要がある場合は、これらのプロパティのいずれかを使用する必要があります。
`watchFiles`配列内のファイルが最後のビルド以降に変更された場合、リビルドがトリガーされます。変更の検出はやや複雑で、ファイルの内容やファイルのメタデータをチェックする場合があります。
`watchDirs`配列内のディレクトリのディレクトリエントリのリストが最後のビルド以降に変更された場合も、リビルドがトリガーされます。これは、これらのディレクトリ内のファイルの内容については何もチェックせず、サブディレクトリもチェックしないことに注意してください。これは、Unixの`ls`コマンドの出力をチェックすると考えてください。
堅牢性を高めるために、プラグインの評価中に使用されたすべてのファイルシステムパスを含める必要があります。たとえば、プラグインが`require.resolve()`と同等の処理を行う場合、最終的なパスだけでなく、すべての「このファイルは存在しますか」チェックのパスを含める必要があります。そうしないと、ビルドが古くなる原因となる新しいファイルが作成される可能性がありますが、esbuildはそのパスがリストされていないため、それを検出しません。
pluginName
このプロパティを使用すると、このモジュールのロード操作のために、このプラグインの名前を別の名前に置き換えることができます。これは、このプラグインを介して別のプラグインをプロキシする場合に便利です。たとえば、複数のプラグインを含む子プロセスに転送する単一のプラグインを持つことができます。おそらくこれを使用する必要はありません。
pluginData
このプロパティは、プラグインチェーンで実行される次のプラグインに渡されます。`onLoad`プラグインから返された場合、そのファイルのインポートに対して`onResolve`プラグインに渡され、`onResolve`プラグインから返された場合、ファイルのロード時に任意のものが`onLoad`プラグインに渡されます(関係が多対一であるため、任意です)。これは、異なるプラグインが直接調整することなく、データをやり取りするのに役立ちます。
#プラグインのキャッシュ
esbuild は非常に高速であるため、esbuild でビルドする場合、プラグインの評価が主なボトルネックになることがよくあります。キャッシュの無効化はプラグイン固有であるため、プラグインの評価のキャッシュは esbuild 自体の一部ではなく、各プラグインに任されています。高速化のためにキャッシュを必要とする低速なプラグインを作成している場合は、キャッシュロジックを自分で記述する必要があります。
キャッシュは、基本的にプラグインを表す変換関数をメモ化するマップです。マップのキーには通常、変換関数の入力が含まれ、マップの値には通常、変換関数の出力が含まれます。さらに、マップには通常、時間の経過とともにサイズが大きくなり続けるのを防ぐために、何らかの形式の最低使用頻度キャッシュエビクションポリシーがあります。
キャッシュは、メモリ(esbuild の 再構築 API での使用に有益)、ディスク(個別のビルドスクリプト呼び出し間でのキャッシュに有益)、またはサーバー(異なる開発者マシン間で共有できる非常に遅い変換に有益)に保存できます。キャッシュをどこに保存するかはケース固有であり、プラグインによって異なります。
簡単なキャッシュの例を次に示します。*.example
形式のファイルの内容を入力として受け取り、JavaScript に変換する関数 slowTransform()
をキャッシュするとします。esbuild の 再構築 API で使用した場合に、この関数への冗長な呼び出しを回避するメモリ内キャッシュは、次のようになります。
import fs from 'node:fs'
let examplePlugin = {
name: 'example',
setup(build) {
let cache = new Map
build.onLoad({ filter: /\.example$/ }, async (args) => {
let input = await fs.promises.readFile(args.path, 'utf8')
let key = args.path
let value = cache.get(key)
if (!value || value.input !== input) {
let contents = slowTransform(input)
value = { input, output: { contents } }
cache.set(key, value)
}
return value.output
})
}
}
上記のキャッシュコードに関するいくつかの重要な注意点
上記のコードにはキャッシュエビクションポリシーがありません。キャッシュマップに increasingly多くのキーが追加されると、メモリ使用量は増加し続けます。
input
値はキャッシュkey
ではなくキャッシュvalue
に格納されます。これは、キーにはファイルの内容ではなくファイルパスのみが含まれているため、ファイルの内容を変更してもメモリリークが発生しないことを意味します。ファイルの内容を変更すると、以前のキャッシュエントリが上書きされるだけです。これは、誰かが増分再構築の間に同じファイルを繰り返し編集し、ファイルの追加や名前の変更をたまに行うだけの一般的な使用法ではおそらく問題ありません。キャッシュの無効化は、
slowTransform()
が 純粋関数(関数の出力は関数の入力のみに依存することを意味する)であり、関数のすべての入力がキャッシュマップへのルックアップに何らかの形でキャプチャされている場合にのみ機能します。たとえば、変換関数が他のファイルの内容を自動的に読み取り、出力がそれらのファイルの内容にも依存する場合、それらのファイルがキャッシュキーに含まれていないため、それらのファイルが変更されたときにキャッシュは無効になりません。この部分は混乱しやすいので、具体的な例を見てみましょう。コンパイルから CSS 言語へのプラグインを実装しているとします。そのプラグインが、インポートされたファイルを解析し、それらをバンドルするか、エクスポートされた変数宣言をインポートするコードで使用できるようにすることで、
@import
ルールを独自に実装する場合、インポートされたファイルの内容が変更されていないことを確認するだけでは、プラグインは正しくありません。インポートされたファイルへの変更もキャッシュを無効にする可能性があるためです。この問題を解決するために、インポートされたファイルの内容をキャッシュキーに追加すればよいと思うかもしれません。しかし、それでも正しくない場合があります。たとえば、このプラグインが
require.resolve()
を使用してインポートパスを絶対ファイルパスに解決するとします。これは、パッケージ内のパスを解決できるノードの組み込みパス解決を使用するため、一般的なアプローチです。この関数は通常、解決されたパスを返す前に、さまざまな場所にあるファイルを多数チェックします。たとえば、ファイルsrc/entry.css
からパスpkg/file
をインポートすると、次の場所がチェックされる可能性があります(はい、ノードのパッケージ解決アルゴリズムは非常に非効率的です)。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()
がチェックする他のファイルのいずれかが作成または削除されている場合、キャッシュエントリは失効する可能性がありますキャッシュエントリが追加されてから。これらのキャッシュキーは、メモリ内キャッシュの場合にのみ正しいです。同じキャッシュキーを使用してファイルシステムキャッシュを実装することは正しくありません。メモリ内キャッシュは、コードもメモリに格納されているため、すべてのビルドで常に同じコードを実行することが保証されていますが、ファイルシステムキャッシュは、それぞれ異なるコードを含む 2 つの個別のビルドによってアクセスされる可能性があります。具体的には、
slowTransform()
関数のコードがビルド間で変更されている可能性があります。これはさまざまな場合に発生する可能性があります。関数
slowTransform()
を含むパッケージが更新された可能性があります。または、パッケージのバージョンを固定していても、npm が semver を処理する方法により、その推移的な依存関係の 1 つが更新された可能性があります。または、誰かが ファイルシステム上のパッケージの内容を変更した 可能性があります。または、変換関数がノード 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
にすることができ、promise を返すことができます。すべてのプラグインからのすべての開始時コールバックは同時に実行され、ビルドはすべての開始時コールバックが完了するまで待ってから続行します。開始時コールバックは、必要に応じてエラーや警告を返して、ビルドに含めることができます。
開始時コールバックには、ビルドオプション を変更する機能がないことに注意してください。初期ビルドオプションは setup
関数内でのみ変更でき、setup
が返されると一度だけ使用されます。最初のビルド以降のすべてのビルドは同じ初期オプションを再利用するため、初期オプションは再利用されず、開始コールバック内で行われた build.initialOptions
への変更は無視されます。
#終了時コールバック
新しいビルドの終了時に通知される終了時コールバックを登録します。これは、初期ビルドだけでなく、すべてのビルドでトリガーされるため、再構築、監視モード、および サーブモード に特に役立ちます。終了時コールバックを追加する方法は次のとおりです。
let examplePlugin = {
name: 'example',
setup(build) {
build.onEnd(result => {
console.log(`build ended with ${result.errors.length} errors`)
})
},
}
package main
import "fmt"
import "github.com/evanw/esbuild/pkg/api"
import "os"
var examplePlugin = api.Plugin{
Name: "example",
Setup: func(build api.PluginBuild) {
build.OnEnd(func(result *api.BuildResult) (api.OnEndResult, error) {
fmt.Fprintf(os.Stderr, "build ended with %d errors\n", len(result.Errors))
return api.OnEndResult{}, nil
})
},
}
func main() {
}
すべての終了時コールバックは順番に実行され、各コールバックは最終的なビルド結果にアクセスできます。ビルド結果を返す前に変更し、promise を返すことでビルドの終了を遅らせることができます。ビルドグラフを検査できるようにするには、初期オプション で メタファイル 設定を有効にする必要があります。ビルドグラフは、ビルド結果オブジェクトの metafile
プロパティとして返されます。
#破棄時コールバック
プラグインが使用されなくなったときにクリーンアップを実行するための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のすべてのパス解決ロジックは、node_modules
ディレクトリ内のパッケージの検索を含む(それらのnode_modules
ディレクトリがどこにあるかを知る必要があるため)、resolveDir
パラメーターに依存します。特定のディレクトリ内のファイル名を解決する場合は、入力パスが
./
で始まることを確認してください。そうしないと、入力パスは相対パスではなくパッケージパスとして扱われます。この動作は、esbuildの通常のパス解決ロジックと同じです。パス解決に失敗した場合、返されたオブジェクトの
errors
プロパティは、エラー情報を含む空でない配列になります。この関数は、失敗したときに常にエラーをスローするとは限りません。呼び出し後にエラーを確認する必要があります。この関数の動作は、ビルド構成によって異なります。そのため、トップレベルのAPI呼び出しではなく、
build
オブジェクトのプロパティになっています。これはまた、すべてのプラグインのsetup
関数が完了するまで呼び出すことができないことも意味します。これらの関数は、ビルドの開始時に固定される前に、プラグインがビルド構成を調整する機会を提供するためです。そのため、resolve
関数は、onResolve
コールバックまたはonLoad
コールバック、あるいはその両方の中で最も役立ちます。現在、無限パス解決ループを検出する試みは行われていません。同じパラメーターを使用して
onResolve
内からresolve
を呼び出すことは、ほぼ確実に悪い考えです。
#解決オプション
resolve
関数は、解決するパスを最初の引数として、オプションのプロパティを持つオブジェクトを2番目の引数として受け取ります。このオプションオブジェクトは、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にパスのインポート方法を指示します。これはパス解決に影響を与える可能性があります。たとえば、ノードのパス解決ルールでは、
'require-call'
を使用してインポートされたパスは、package.json
の"require"
セクションの条件付きパッケージインポートを尊重する必要があるのに対し、'import-statement'
を使用してインポートされたパスは、代わりに"import"
セクションの条件付きパッケージインポートを尊重する必要があるとされています。importer
設定されている場合、これは解決されるこのインポートを含むモジュールのパスとして解釈されます。これは、
importer
値をチェックするonResolve
コールバックを持つプラグインに影響します。namespace
設定されている場合、これは解決されるこのインポートを含むモジュールの名前空間として解釈されます。これは、
namespace
値をチェックするonResolve
コールバックを持つプラグインに影響します。名前空間の詳細については、こちらをご覧ください。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
これはパス解決の結果、またはパス解決に失敗した場合は空の文字列です。
これを空でない文字列に設定して、インポートを特定のパスに解決します。これが設定されている場合、このモジュール内のこのインポートパスに対して、これ以上on-resolveコールバックは実行されません。これが設定されていない場合、esbuildは現在のコールバックの後に登録されたon-resolveコールバックを実行し続けます。その後、パスがまだ解決されていない場合、esbuildはデフォルトで現在のモジュールの解決ディレクトリを基準にしてパスを解決します。
パスが外部としてマークされている場合、これは
true
になります。つまり、バンドルには含まれず、実行時にインポートされます。namespace
これは、解決されたパスに関連付けられた名前空間です。名前空間の詳細については、こちらをご覧ください。
- `errors`と`warnings`
これらのプロパティは、このパス解決操作に応答したプラグインまたはesbuild自体によって、パス解決中に生成されたすべてのログメッセージを保持します。これらのログメッセージはログに自動的に含まれないため、破棄すると完全に表示されなくなります。ログに含めたい場合は、
onResolve
またはonLoad
から返す必要があります。 pluginData
プラグインがこのパス解決操作に応答し、
onResolve
コールバックからpluginData
を返した場合、そのデータはここに格納されます。これは、異なるプラグインが直接調整することなく、それらの間でデータを渡すのに役立ちます。sideEffects
このプロパティは、モジュールが副作用がないと注釈が付けられている場合を除き、
true
になります。その場合はfalse
になります。対応するpackage.json
ファイルに"sideEffects": false
があるパッケージ、およびプラグインがこのパス解決操作に応答してsideEffects: false
を返す場合、これはfalse
になります。sideEffects
の意味の詳細については、Webpackの機能に関するドキュメントをご覧ください。suffix
解決されるパスの末尾にURLクエリまたはハッシュがあり、パスを正常に解決するためにそれを削除する必要がある場合、これはオプションの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プラグイン
この例では、バイナリデータの操作、importステートメントを使用した仮想モジュールの作成、異なる名前空間での同じパスの再利用について説明します。
このプラグインを使用すると、.wasm
ファイルをJavaScriptコードにインポートできます。WebAssemblyファイル自体を生成しません。これは、別のツールで行うか、このプラグインの例をニーズに合わせて変更することで行うことができます。次のワークフローを有効にします。
import load from './example.wasm'
load(imports).then(exports => { ... })
.wasm
ファイルをインポートすると、このプラグインは、デフォルトのエクスポートとしてエクスポートされたWebAssemblyモジュールをロードする単一の関数を持つ、wasm-stub
名前空間に仮想JavaScriptモジュールを生成します。そのスタブモジュールは次のようになります。
import wasm from '/path/to/example.wasm'
export default (imports) =>
WebAssembly.instantiate(wasm, imports).then(
result => result.instance.exports)
次に、そのスタブモジュールは、esbuildの組み込みバイナリローダーを使用して、wasm-binary
名前空間の別のモジュールとしてWebAssemblyファイル自体をインポートします。これは、.wasm
ファイルをインポートすると、実際には2つの仮想モジュールが生成されることを意味します。プラグインのコードは次のとおりです。
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
名前空間のロードコールバックは、ローダー関数をエクスポートし、.wasm
パスをインポートするJavaScriptスタブモジュールを生成します. これにより、解決コールバックが再び呼び出され、今回はパスがwasm-binary
名前空間に移動します. 次に、wasm-binary
名前空間の2番目のロードコールバックにより、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
パッケージに依存し、コンポーネントをdefault
エクスポートを使用して単一のクラスとしてエクスポートするJavaScriptモジュールが生成されます。これは、.svelte
ファイルを独立してコンパイルできることを意味し、Svelteはesbuildプラグインに適しています。このプラグインは、次のように.svelte
ファイルをインポートすることによってトリガーされます。
import Button from './button.svelte'
プラグインのコードは次のとおりです(SvelteコンパイラはJavaScriptで記述されているため、このプラグインのGoバージョンはありません)。
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はこれを使用して、最終的なソースマップで生成された位置が中間JavaScriptコードではなく、元のSvelteファイルにマッピングされるようにします。
#プラグインAPIの制限
このAPIは、すべてのユースケースを網羅することを意図していません。バンドルプロセスのすべての部分にフックすることはできません。たとえば、現在、ASTを直接変更することはできません。この制限は、esbuildの優れたパフォーマンス特性を維持するため、およびメンテナンスの負担となり、ASTの変更を伴う改善を妨げる可能性のある過剰なAPIサーフェスを公開しないようにするために存在します。
esbuildを捉える1つの方法は、Webの「リンカ」と考えることです。ネイティブコードのリンカと同様に、esbuildの役割は、一連のファイルを取得し、それらの間の参照を解決してバインドし、すべてのコードがリンクされた単一のファイルを生成することです。プラグインの役割は、最終的にリンクされる個々のファイルを生成することです。
esbuildのプラグインは、比較的スコープが限定されており、ビルドの小さな側面のみをカスタマイズする場合に最適に機能します。たとえば、カスタム形式(例:YAML)の特別な設定ファイル用のプラグインは非常に適切です。使用するプラグインが多いほど、特にプラグインがJavaScriptで記述されている場合、ビルドは遅くなります。プラグインがビルド内のすべてのファイルに適用される場合、ビルドは非常に遅くなる可能性があります。キャッシングが適用可能な場合、プラグイン自体によって行う必要があります。