コンテンツタイプ

以下に、すべての組み込みコンテンツタイプをリストアップします。各コンテンツタイプには、ファイルの内容をどのように解釈するかをesbuildに指示する関連付けられた「ローダー」があります。一部のファイル拡張子は、デフォルトでローダーが既に構成されていますが、デフォルトは上書きできます。

JavaScript

ローダー: js

このローダーは、.js.cjs、および.mjsファイルに対してデフォルトで有効になっています。.cjs拡張子は、Node.jsでCommonJSモジュールに使用され、.mjs拡張子は、Node.jsでECMAScriptモジュールに使用されます。

デフォルトでは、esbuildの出力はすべての最新のJS機能を活用します。たとえば、a !== void 0 && a !== null ? a : bは、ミニファイが有効になっている場合、a ?? bになります。これはES2020バージョンのJavaScriptの構文を使用しています。これが望ましくない場合は、esbuildのtarget設定を指定して、出力で正しく動作する必要があるブラウザを指定する必要があります。そうすると、esbuildはそれらのブラウザにとって古すぎるJavaScript機能の使用を避けられます。

esbuildは、最新のJavaScript構文をすべてサポートしています。ただし、新しい構文は古いブラウザではサポートされない場合があるため、targetオプションを設定して、必要に応じて新しい構文を古い構文に変換するようにesbuildに指示することをお勧めします。

これらの構文機能は、古いブラウザに対して常に変換されます。

構文変換 言語バージョン
関数のパラメータリストと呼び出しにおける末尾のカンマ es2017 foo(a, b, )
数値セパレータ esnext 1_000_000

これらの構文機能は、構成された言語targetに応じて、古いブラウザに対して条件付きで変換されます。

構文変換 --targetが以下の場合に変換されます。
べき乗演算子 es2016 a ** b
非同期関数 es2017 async () => {}
非同期イテレーション es2018 for await (let x of y) {}
非同期ジェネレータ es2018 async function* foo() {}
スプレッドプロパティ es2018 let x = {...y}
レストプロパティ es2018 let {...x} = y
省略可能なcatchバインディング es2019 try {} catch {}
オプショナルチェイニング es2020 a?.b
Nullish 合体演算子 es2020 a ?? b
import.meta es2020 import.meta
論理代入演算子 es2021 a ??= b
クラスインスタンスフィールド es2022 class { x }
静的クラスフィールド es2022 class { static x }
プライベートインスタンスメソッド es2022 class { #x() {} }
プライベートインスタンスフィールド es2022 class { #x }
プライベート静的メソッド es2022 class { static #x() {} }
プライベート静的フィールド es2022 class { static #x }
人間工学的なブランドチェック es2022 #x in y
クラス静的ブロック es2022 class { static {} }
importアサーション esnext import "x" assert {}
自動アクセサ esnext class { accessor x }
using宣言 esnext using x = y

これらの構文機能は、現在、常に変換されずに渡されます。

構文変換 --targetが以下の場合、サポートされていません。
RegExp dotAllフラグ es2018 /./s1
RegExp後方参照アサーション es2018 /(?<=x)y/1
RegExp名前付きキャプチャグループ es2018 /(?<foo>\d+)/1
RegExp Unicodeプロパティエスケープ es2018 /\p{ASCII}/u1
BigInt es2020 123n
トップレベルのawait es2022 await import(x)
任意のモジュール名前空間識別子 es2022 export {foo as 'f o o'}
RegExpマッチインデックス es2022 /x(.+)y/d1
ハッシュバング構文 esnext #!/usr/bin/env node
デコレータ esnext @foo class Bar {}
RegExp集合表記 esnext /[\w--\d]/1

完了したECMAScript提案のリストアクティブなECMAScript提案のリストも参照してください。トップレベルのawaitを含むコードの変換はサポートされていますが、トップレベルのawaitを含むコードのバンドルは、出力形式esmに設定されている場合にのみサポートされていることに注意してください。

JavaScriptの注意点

esbuildでJavaScriptを使用する際には、次の点に注意してください。

ES5は十分にサポートされていません

ES6+構文をES5に変換することはまだサポートされていません。ただし、esbuildを使用してES5コードを変換する場合は、targetes5に設定する必要があります。これにより、esbuildがES6構文をES5コードに導入することがなくなります。たとえば、このフラグがないと、オブジェクトリテラル{x: x}{x}になり、文字列"a\nb"はミニファイ時に複数行のテンプレートリテラルになります。これらの置換はどちらも結果のコードが短くなるために行われますが、targetes5の場合は、これらの置換は行われません。

プライベートメンバーのパフォーマンス

プライベートメンバー変換(#name構文用)は、WeakMapWeakSetを使用して、この機能のプライバシープロパティを保持します。これは、BabelとTypeScriptコンパイラにおける対応する変換に似ています。最新のJavaScriptエンジン(V8、JavaScriptCore、およびSpiderMonkey、ただしChakraCoreではない)は、大規模なWeakMapWeakSetオブジェクトに対して良好なパフォーマンス特性を持たない場合があります。

この構文変換がアクティブな状態で、プライベートフィールドまたはプライベートメソッドを持つクラスのインスタンスを多数作成すると、ガベージコレクタにとって大きなオーバーヘッドが発生する可能性があります。これは、最新のエンジン(ChakraCore以外)が弱い値を実際のマップオブジェクトに格納し、キー自体の非表示プロパティとして格納しないためであり、大規模なマップオブジェクトはガベージコレクションでパフォーマンスの問題を引き起こす可能性があります。この参照で詳細を確認してください。

インポートはECMAScriptモジュールの動作に従います

そのグローバル状態を必要とするモジュールをインポートする前にグローバル状態を変更しようとすると、動作すると期待するかもしれません。しかし、JavaScript(したがってesbuild)はすべてのimport文をファイルの先頭に効果的に「ホイスティング」するため、これは機能しません。

window.foo = {}
import './something-that-needs-foo'

この点でJavaScript仕様に従っていない、ECMAScriptモジュールの壊れた実装がいくつか存在します(たとえば、TypeScriptコンパイラ)。これらのツールでコンパイルされたコードは、importrequire()へのインライン呼び出しで置き換えられるため、「動作する」可能性があります。これはホイスティングの要件を無視します。しかし、そのようなコードは、Node.js、ブラウザ、またはesbuildなどの実際のECMAScriptモジュール実装では動作しないため、このようなコードの記述は移植性がなく、推奨されません。

これを正しく行う方法は、グローバル状態の変更を独自のインポートに移動することです。そうすれば、他のインポートの前に実行されます。

import './assign-to-foo-on-window'
import './something-that-needs-foo'

バンドル時に直接evalを避ける

eval(x)は通常の関数呼び出しのように見えますが、実際にはJavaScriptで特別な動作をします。このようにevalを使用すると、xに格納された評価されたコードは、名前で任意の包含スコープ内の任意の変数を参照できます。たとえば、コードlet y = 123; return eval('y')123を返します。

これは「直接eval」と呼ばれ、多くの理由からコードをバンドルする際に問題となります。

幸いなことに、通常は直接のevalの使用を回避するのは簡単です。上記のすべての欠点を回避する、一般的に使用される2つの代替手段があります。

toString()の値は、関数(およびクラス)では保持されません。

JavaScriptの関数オブジェクトでtoString()を呼び出し、その文字列を何らかの形式のevalに渡して新しい関数オブジェクトを取得することは、ある程度一般的です。これは、関数を包含ファイルから「引き剥がして」、そのファイル内のすべての変数とのリンクを切断する効果があります。esbuildでのこの操作はサポートされておらず、機能しない可能性があります。特に、esbuildはヘルパーメソッドを使用して特定の機能を実装することが多く、JavaScriptのスコープルールが改ざんされていないことを前提としています。たとえば

let pow = (a, b) => a ** b;
let pow2 = (0, eval)(pow.toString());
console.log(pow2(2, 3));

このコードがES6向けにコンパイルされる場合、**演算子は使用できないため、**演算子は__powヘルパー関数の呼び出しに置き換えられます。

let __pow = Math.pow;
let pow = (a, b) => __pow(a, b);
let pow2 = (0, eval)(pow.toString());
console.log(pow2(2, 3));

このコードを実行しようとすると、ReferenceError: __pow is not definedなどのエラーが発生します。これは、関数(a, b) => __pow(a, b)がローカルスコープのシンボル__powに依存しており、グローバルスコープでは使用できないためです。これは、async関数を含む多くのJavaScript言語機能、および名前を保持する設定などの一部のesbuild固有の機能にも当てはまります。

この問題は、ほとんどの場合、人々が.toString()を使用して関数のソースコードを取得し、それをウェブワーカーの本文として使用しようとした場合に発生します。esbuildを使用したい場合、ウェブワーカーのソースコードを別のビルドステップでビルドし、ウェブワーカーを作成するコードにウェブワーカーのソースコードを文字列として挿入する必要があります。define機能は、ビルド時に文字列を挿入する1つの方法です。

モジュール名前空間オブジェクトから呼び出された関数のthisの値は保持されません。

JavaScriptでは、関数内のthisの値は、関数の呼び出し方法に基づいて自動的に入力されます。たとえば、関数がobj.fn()を使用して呼び出された場合、関数呼び出し中のthisの値はobjになります。この動作はesbuildによって尊重されますが、例外があります。モジュール名前空間オブジェクトから関数を呼び出す場合、thisの値が正しくない可能性があります。たとえば、モジュール名前空間オブジェクトnsからfooを呼び出す次のコードを考えてみます。

import * as ns from './foo.js'
ns.foo()

foo.jsがモジュール名前空間オブジェクトをthisを使用して参照しようとすると、esbuildでコードをバンドルした後は必ずしも機能しません。

// foo.js
export function foo() {
  this.bar()
}
export function bar() {
  console.log('bar')
}

その理由は、esbuildがモジュール名前空間オブジェクトを使用するほとんどのコードを、代わりに直接インポートするコードに自動的に書き換えるためです。つまり、上記の例コードは代わりに次のように変換され、関数呼び出しのthisコンテキストが削除されます。

import { foo } from './foo.js'
foo()

この変換により、エクスポートされたシンボルの未使用部分をesbuildが理解できるようになるため、ツリーシェイキング(別名デッドコード除去)が大幅に向上します。関数呼び出しの`this`コンテキストが削除されます。この変換により、エクスポートされたシンボルの未使用部分をesbuildが理解できるようになるため、ツリーシェイキング(別名デッドコード除去)が大幅に向上します。ただし、thisを使用してモジュールのエクスポートにアクセスするコードの動作が変更されるという欠点があります。ただし、そもそも誰もこのような奇妙なコードを書くべきではないため、これは問題ではありません。同じファイルからエクスポートされた関数にアクセスする必要がある場合は、直接呼び出すだけです(つまり、上記の例ではthis.bar()ではなくbar())。

defaultエクスポートはエラーが発生しやすい可能性があります。

ESモジュール形式(つまりESM)には、他のすべてのエクスポート名とは異なる動作をするdefaultと呼ばれる特別なエクスポートがあります。defaultエクスポートを持つESM形式のコードをCommonJS形式に変換し、そのCommonJSコードをESM形式の別のモジュールにインポートすると、両方とも広く使用されている2つの異なる解釈があります(Babelの方法とNodeの方法)。これは非常に残念です。特に、JavaScriptライブラリは多くの場合ESMで作成され、CommonJSとして公開されるため、終わりのない互換性の頭痛を引き起こすためです。

esbuildがコードをバンドルする場合、どの解釈を使用するかを決定する必要があり、完璧な答えはありません。esbuildが使用するヒューリスティックは、Webpackが使用するヒューリスティックと同じです(詳細は以下を参照)。Webpackは最も広く使用されているバンドラであるため、esbuildは、この互換性の問題に関して、既存のエコシステムと最大限に互換性を持つことができます。そのため、この問題のあるコードをesbuildで動作させることができれば、Webpackでも動作するはずです。

問題は次の例で示されています。

// index.js
import foo from './somelib.js'
console.log(foo)
// somelib.js
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports["default"] = 'foo';

そして、これら2つの解釈はどちらも広く使用されています。

ライブラリの作成者向け:新しいコードを作成する場合は、defaultエクスポートを完全に回避することを強く検討してください。残念ながら、互換性の問題に悩まされており、使用すると、いつかユーザーに問題を引き起こす可能性があります。

ライブラリのユーザー向け:デフォルトで、esbuildはBabelの解釈を使用します。代わりにNodeの解釈を使用するようにesbuildに指示するには、コードを.mtsまたは.mjsで終わるファイルに配置するか、"type": "module"package.jsonファイルに追加する必要があります。その理由は、NodeのネイティブESMサポートは、ファイル拡張子が.mjsであるか、"type": "module"が存在する場合にのみESMコードを実行できるため、それを行うことは、コードがNodeで実行されることを意図していることを示す良いシグナルであり、したがってNodeのdefaultインポートの解釈を使用する必要があります。これは、Webpackが使用するヒューリスティックと同じです。

TypeScript

ローダー:tsまたはtsx

このローダーは、.ts.tsx.mts.ctsファイルに対してデフォルトで有効になっているため、esbuildにはTypeScript構文を解析し、型注釈を破棄するための組み込みサポートがあります。ただし、esbuildは型チェックを行わないため、型をチェックするには、esbuildと並行してtsc -noEmitを実行する必要があります。これは、esbuild自体が行うものではありません。

これらのTypeScript型宣言は解析され、無視されます(網羅的でないリスト)。

構文機能
インターフェース宣言 interface Foo {}
型宣言 type Foo = number
関数宣言 function foo(): void;
環境宣言 declare module 'foo' {}
型のみのインポート import type {Type} from 'foo'
型のみのエクスポート export type {Type} from 'foo'
型のみのインポート指定子 import {type Type} from 'foo'
型のみのエクスポート指定子 export {type Type} from 'foo'

TypeScript固有の構文拡張がサポートされており、常にJavaScriptに変換されます(網羅的でないリスト)。

構文機能 備考
名前空間 namespace Foo {}
列挙型 enum Foo { A, B }
定数列挙型 const enum Foo { A, B }
ジェネリック型パラメータ <T>(a: T): T => a tsxローダーでは<T,>(...と記述する必要があります。
型付きJSX <Element<T>/>
型キャスト a as B<B>a
型インポート import {Type} from 'foo' 未使用のインポートをすべて削除することで処理されます。
型エクスポート export {Type} from 'foo' TypeScriptファイルで不足しているエクスポートを無視することで処理されます。
実験的なデコレータ @sealed class Foo {} experimentalDecoratorsが必要です。
emitDecoratorMetadataはサポートされていません。
インスタンス化式 Array<number> TypeScript 4.7以降
inferでのextends infer A extends B TypeScript 4.7以降
分散注釈 type A<out B> = () => B TypeScript 4.7以降
satisfies 演算子 a satisfies T TypeScript 4.9以降
const 型パラメータ class Foo<const T> {} TypeScript 5.0以降

TypeScriptに関する注意点

esbuildでTypeScriptを使用する際は、以下の点に注意してください(JavaScriptに関する注意点に加えて)。

ファイルは独立してコンパイルされる

単一のモジュールをトランスパイルする場合でも、TypeScriptコンパイラはインポートされたファイルを解析して、インポートされた名前が型であるか値であるかを判断します。しかし、esbuildやBabel(そしてTypeScriptコンパイラのtranspileModule API)のようなツールは、各ファイルを独立してコンパイルするため、インポートされた名前が型であるか値であるかを判断できません。

このため、esbuildでTypeScriptを使用する場合は、isolatedModules TypeScript構成オプションを有効にする必要があります。このオプションは、ファイルごとに独立して型参照をトレースせずにコンパイルされるesbuildなどの環境で、誤ったコンパイルを引き起こす可能性のある機能の使用を防止します。たとえば、export {T} from './types'を使用して別のモジュールから型を再エクスポートすることを防ぎます(代わりにexport type {T} from './types'を使用する必要があります)。

インポートはECMAScriptモジュールの動作に従う

歴史的な理由から、TypeScriptコンパイラはデフォルトでESM(ECMAScriptモジュール)構文をCommonJS構文にコンパイルします。たとえば、import * as foo from 'foo'const foo = require('foo')にコンパイルされます。これはおそらく、TypeScriptが構文を採用した時点でECMAScriptモジュールはまだ提案段階だったためです。しかし、これはレガシーな動作であり、nodeなどの実際のプラットフォームでのこの構文の動作とは一致しません。たとえば、require関数は文字列を含む任意のJavaScript値を返すことができますが、import * as構文は常にオブジェクトを返し、文字列になることはありません。

このレガシー機能による問題を回避するには、esbuildでTypeScriptを使用する場合は、esModuleInterop TypeScript構成オプションを有効にする必要があります。これを有効にすると、このレガシー動作が無効になり、TypeScriptの型システムがESMと互換性を持つようになります。このオプションは、既存のTypeScriptプロジェクトにとって破壊的変更となるため、デフォルトでは有効になっていませんが、Microsoftは新規プロジェクトと既存プロジェクトの両方への適用(そしてその後のコードの更新)を強く推奨しており、エコシステムの他の部分との互換性を向上させます。

具体的には、CommonJSモジュールからESMインポート構文を使用して非オブジェクト値をインポートする場合は、import * asを使用する代わりに、デフォルトインポートを使用する必要があります。したがって、CommonJSモジュールがmodule.exports = fnを介して関数をエクスポートする場合は、import * as fn from 'path'ではなくimport fn from 'path'を使用する必要があります。

型システムを必要とする機能はサポートされない

TypeScriptの型はコメントとして扱われ、esbuildによって無視されるため、TypeScriptは「型チェックされたJavaScript」として扱われます。型アノテーションの解釈はTypeScript型チェッカーに依存し、TypeScriptを使用する場合はesbuildに加えて実行する必要があります。これは、BabelのTypeScript実装が使用するのと同じコンパイル戦略です。ただし、これは、動作に型の解釈が必要なTypeScriptのコンパイル機能の一部がesbuildでは動作しないことを意味します。

具体的には

特定のtsconfig.jsonフィールドのみが尊重される

バンドリング中に、esbuildのパス解決アルゴリズムは、最も近い親ディレクトリにあるtsconfig.jsonファイルの内容を考慮し、その動作をそれに応じて変更します。esbuildのtsconfig設定を使用してビルドAPIでtsconfig.jsonパスの設定を明示的に行い、esbuildのtsconfigRaw設定を使用して変換APIでtsconfig.jsonファイルの内容を明示的に渡すことも可能です。ただし、esbuildは現在、tsconfig.jsonファイルで以下のフィールドのみを検査します。

その他のtsconfig.jsonフィールド(上記リストにないもの)は無視されます。

*.tsファイルにtsxローダーを使用することはできません

tsxローダーはtsローダーの上位セットではありません。これらは、部分的に互換性のない2つの異なる構文です。たとえば、文字列<a>1</a>/gは、tsローダーでは<a>(1 < (/a>/g))として、tsxローダーでは(<a>1</a>) / gとして解析されます。

これによって最もよく発生する問題は、tsxローダーで<T>() => {}などのアロー関数式でジェネリック型パラメータを使用できないことです。これは意図的なものであり、公式のTypeScriptコンパイラの動作と一致します。tsx構文におけるそのスペースは、JSX要素のために予約されています。

JSX

ローダー:jsxまたはtsx

JSXは、Reactのために作成された、JavaScriptのXMLのような構文拡張です。ビルドツールによって通常のJavaScriptに変換されることを目的としています。各XML要素は、通常のJavaScript関数呼び出しになります。たとえば、次のJSXコードは

import Button from './button'
let button = <Button>Click me</Button>
render(button)

次のJavaScriptコードに変換されます

import Button from "./button";
let button = React.createElement(Button, null, "Click me");
render(button);

このローダーは、.jsxファイルと.tsxファイルに対してデフォルトで有効になっています。.jsファイルでは、デフォルトでJSX構文は有効になっていないことに注意してください。有効にしたい場合は、設定する必要があります。

CLI JS Go
esbuild app.js --bundle --loader:.js=jsx
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  loader: { '.js': 'jsx' },
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    Loader: map[string]api.Loader{
      ".js": api.LoaderJSX,
    },
    Write: true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

JSXの自動インポート

JSX構文を使用するには、通常、使用しているJSXライブラリを手動でインポートする必要があります。たとえば、Reactを使用している場合、デフォルトでは次のように各JSXファイルにReactをインポートする必要があります。

import * as React from 'react'
render(<div/>)

これは、JSX変換によってJSX構文がReact.createElementへの呼び出しに変換されるためですが、それ自体では何もインポートしないため、React変数は自動的に存在しません。

各ファイルにJSXライブラリを手動でimportするのを避けたい場合は、esbuildのJSX変換をautomaticに設定することで、インポート文を自動生成できます。ただし、これによりJSX変換の動作が完全に変わるため、React以外のJSXライブラリを使用している場合、コードが壊れる可能性があります。設定方法は次のとおりです。

CLI JS Go
esbuild app.jsx --jsx=automatic
require('esbuild').buildSync({
  entryPoints: ['app.jsx'],
  jsx: 'automatic',
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.jsx"},
    JSX:         api.JSXAutomatic,
    Outfile:     "out.js",
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Reactを使わないJSXの使用

React以外のライブラリ(Preactなど)でJSXを使用している場合は、デフォルトでそれぞれReact.createElementReact.FragmentになっているJSXファクトリJSXフラグメントの設定を構成する必要があります。

CLI JS Go
esbuild app.jsx --jsx-factory=h --jsx-fragment=Fragment
require('esbuild').buildSync({
  entryPoints: ['app.jsx'],
  jsxFactory: 'h',
  jsxFragment: 'Fragment',
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.jsx"},
    JSXFactory:  "h",
    JSXFragment: "Fragment",
    Write:       true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

あるいは、TypeScriptを使用している場合は、これをtsconfig.jsonファイルに追加するだけでTypeScriptのJSXを構成でき、esbuildは設定なしで自動的に認識します。

{
  "compilerOptions": {
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment"
  }
}

上記で説明した自動インポートを使用しない限り、JSX構文を含むファイルにimport {h, Fragment} from 'preact'を追加する必要があります。

JSON

ローダー: json

このローダーは、.jsonファイルに対してデフォルトで有効になっています。ビルド時にJSONファイルをJavaScriptオブジェクトに解析し、そのオブジェクトをデフォルトエクスポートとしてエクスポートします。使用方法は次のようになります。

import object from './example.json'
console.log(object)

デフォルトエクスポートに加えて、JSONオブジェクトの各最上位プロパティの名前付きエクスポートもあります。名前付きエクスポートを直接インポートすると、esbuildは使用されていないJSONファイルの部分をバンドルから自動的に削除し、実際に使用した名前付きエクスポートのみを残すことができます。たとえば、このコードはバンドル時にversionフィールドのみを含みます。

import { version } from './package.json'
console.log(version)

CSS

ローダー: cssCSSモジュールの場合はglobal-csslocal-cssも)

cssローダーは.cssファイルに対して、local-cssローダーは.module.cssファイルに対してデフォルトで有効になっています。これらのローダーは、ファイルをCSS構文として読み込みます。CSSはesbuildにおける第一級のコンテンツタイプであるため、esbuildはJavaScriptコードからCSSをインポートする必要なく、バンドルできます。

CLI JS Go
esbuild --bundle app.css --outfile=out.css
require('esbuild').buildSync({
  entryPoints: ['app.css'],
  bundle: true,
  outfile: 'out.css',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.css"},
    Bundle:      true,
    Outfile:     "out.css",
    Write:       true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

他のCSSファイルを@importしたり、url()で画像やフォントファイルを参照したりできます。esbuildはすべてをまとめてバンドルします。ただし、esbuildには事前に設定されたものがないため、画像やフォントファイルのローダーを設定する必要があります。通常は、data URLローダーまたは外部ファイルローダーのいずれかです。

これらの構文機能は、構成された言語targetに応じて、古いブラウザに対して条件付きで変換されます。

構文変換
ネストされた宣言 a { &:hover { color: red } }
最新のRGB/HSL構文 #F008
inset shorthand inset: 0
hwb() hwb(120 30% 50%)
lab()lch() lab(60 -5 58)
oklab()oklch() oklab(0.5 -0.1 0.1)
color() color(display-p3 1 0 0)
2つの位置を持つカラーストップ linear-gradient(red 2% 4%, blue)
グラデーションの遷移ヒント linear-gradient(red, 20%, blue) 1
グラデーションのカラースペース linear-gradient(in hsl, red, blue) 1
グラデーションのヒューモード linear-gradient(in hsl longer hue, red, blue) 1

デフォルトでは、esbuildの出力は最新のCSS機能を利用します。たとえば、color: rgba(255, 0, 0, 0.4)は、minifyが有効になっているとcolor: #f006になります。これはCSS Color Module Level 4の構文を使用しています。これが望ましくない場合は、esbuildのtarget設定を指定して、出力で正しく動作する必要があるブラウザを指定する必要があります。そうすると、esbuildはそれらのブラウザでは古すぎるCSS機能の使用を避けられます。

target設定を使用してブラウザのバージョンのリストを提供すると、esbuildはベンダープレフィックスを自動的に挿入して、CSSがそれらのブラウザのそれらのバージョン以降で動作するようにします。現在、esbuildは次のCSSプロパティに対してこれを行います。

JavaScriptからのインポート

JavaScriptからCSSをインポートすることもできます。これを行うと、esbuildは特定のエントリーポイントから参照されているすべてのCSSファイルを収集し、そのJavaScriptエントリーポイントに対応するJavaScript出力ファイルの隣に、対応するCSS出力ファイルにバンドルします。そのため、esbuildがapp.jsを生成する場合、app.jsから参照されているすべてのCSSファイルを含むapp.cssも生成します。JavaScriptからCSSファイルをインポートする例を次に示します。

import './button.css'

export let Button = ({ text }) =>
  <div className="button">{text}</div>

esbuildによって生成されたバンドルされたJavaScriptは、生成されたCSSをHTMLページに自動的にインポートしません。代わりに、生成されたJavaScriptと共に、生成されたCSSをHTMLページに手動でインポートする必要があります。これにより、ブラウザはCSSファイルとJavaScriptファイルを並列でダウンロードできるため、最も効率的な方法です。方法は次のようになります。

<html>
  <head>
    <link href="app.css" rel="stylesheet">
    <script src="app.js"></script>
  </head>
</html>

生成された出力名が単純ではない場合(たとえば、entry names設定に[hash]を追加し、出力ファイル名にコンテンツハッシュが含まれている場合)、metafileで生成された出力名を確認する必要があるでしょう。これを行うには、まずentryPointプロパティと一致する出力を検索してJSファイルを見つけます。このファイルは<script>タグに入ります。関連付けられたCSSファイルは、cssBundleプロパティを使用して見つけることができます。このファイルは<link>タグに入ります。

CSSモジュール

CSSモジュールは、意図しないCSS名の衝突を回避するためのCSSプリプロセッサ技術です。CSSクラス名は通常グローバルですが、CSSモジュールは、代わりにCSSクラス名をファイル内に限定する方法を提供します。2つの別々のCSSファイルが同じローカルクラス名.buttonを使用する場合、esbuildはそれらのいずれか1つを自動的に名前変更して、衝突しないようにします。これは、esbuildが名前の衝突を避けるために、別々のJSモジュールで同じ名前のローカル変数を自動的に名前変更する方法に似ています。

esbuildにはCSSモジュールでのバンドリングのサポートがあります。これを使用するには、バンドルを有効にし、CSSファイルにlocal-cssローダーを使用する(たとえば、.module.cssファイル拡張子を使用する)必要があり、次にCSSモジュールコードをJSファイルにインポートします。そのファイル内の各ローカルCSS名は、esbuildによって名前変更された名前を取得するためにJSにインポートできます。例を次に示します。

// app.js
import { outerShell } from './app.module.css'
const div = document.createElement('div')
div.className = outerShell
document.body.appendChild(div)
/* app.module.css */
.outerShell {
  position: absolute;
  inset: 0;
}

これをesbuild app.js --bundle --outdir=outでバンドルすると、次のようになります(ローカルCSS名outerShellがどのように名前変更されているかに注意してください)。

// out/app.js
(() => {
  // app.module.css
  var outerShell = "app_outerShell";

  // app.js
  var div = document.createElement("div");
  div.className = outerShell;
  document.body.appendChild(div);
})();
/* out/app.css */
.app_outerShell {
  position: absolute;
  inset: 0;
}

この機能は、コードが名前変更されたローカル名をimportして使用しなければならないため、そしてesbuildが衝突を回避するために競合するローカル名を正常に名前変更できるように、すべてのローカル名を含むCSSファイルを単一のバンドル操作で処理できる必要があるため、バンドルが有効になっている場合にのみ意味があります。

esbuildがローカルCSS名に対して生成する名前は実装の詳細であり、どこにもハードコードされることを意図していません。JSまたはHTMLでローカルCSS名を参照する唯一の方法は、上記のようにesbuildでバンドルされたJSでのインポートステートメントを使用することです。minifyが有効になっている場合、esbuildは可能な限り短い名前を生成する名前生成アルゴリズムを使用します(esbuildがJSのローカル識別子をminifyする方法に似ています)。

グローバル名の使用

local-cssローダーは、デフォルトでファイル内のすべてのCSS名をローカルにします。ただし、同じファイルでローカル名とグローバル名を混在させたい場合があります。これを行うにはいくつかの方法があります。

いくつかの例を次に示します。

/*
 * This is a local name with the "local-css" loader
 * and a global name with the "global-css" loader
 */
.button {
}

/* This is a local name with both loaders */
:local(.button) {
}

/* This is a global name with both loaders */
:global(.button) {
}

/* "foo" is global and "bar" is local */
:global .foo :local .bar {
}

/* "foo" is global and "bar" is local */
:global {
  .foo {
    :local {
      .bar {}
    }
  }
}

composesディレクティブ

CSSモジュール仕様では、composesディレクティブも説明されています。これにより、ローカル名を持つクラスセレクターが他のクラスセレクターを参照できます。これは、共通のプロパティのセットを分割して重複を回避するために使用できます。また、fromキーワードを使用すると、他のファイルのローカル名を持つクラスセレクターを参照するためにも使用できます。例を次に示します。

// app.js
import { submit } from './style.css'
const div = document.createElement('div')
div.className = submit
document.body.appendChild(div)
/* style.css */
.button {
  composes: pulse from "anim.css";
  display: inline-block;
}
.submit {
  composes: button;
  font-weight: bold;
}
/* anim.css */
@keyframes pulse {
  from, to { opacity: 1 }
  50% { opacity: 0.5 }
}
.pulse {
  animation: 2s ease-in-out infinite pulse;
}

これをesbuild app.js --bundle --outdir=dist --loader:.css=local-cssでバンドルすると、次のようになります。

(() => {
  // style.css
  var submit = "anim_pulse style_button style_submit";

  // app.js
  var div = document.createElement("div");
  div.className = submit;
  document.body.appendChild(div);
})();
/* anim.css */
@keyframes anim_pulse {
  from, to {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}
.anim_pulse {
  animation: 2s ease-in-out infinite anim_pulse;
}

/* style.css */
.style_button {
  display: inline-block;
}
.style_submit {
  font-weight: bold;
}

composesを使用すると、JavaScriptにインポートされる文字列が、合成されたすべてのローカル名のスペース区切りのリストになることに注意してください。これは、DOM要素のclassNameプロパティに渡すことを目的としています。また、fromを使用してcomposesを使用すると、他のCSSファイルのローカル名を(間接的に)参照できることに注意してください。

別々のファイルから合成されたCSSクラスがバンドルされた出力ファイルに表示される順序は、設計上意図的に未定義であることに注意してください(詳細は仕様を参照)。2つの別々のクラスセレクターで同じCSSプロパティを宣言してからそれらを合成することは想定されていません。重複しないCSSプロパティを宣言するCSSクラスセレクターのみを合成する必要があります。

CSSに関する注意点

esbuildでCSSを使用する際には、以下の点に注意してください。

限定的なCSS検証

CSSには、すべてのCSSプロセッサが使用する一般的な構文仕様と、特定のCSSルールの意味を定義する多くの仕様があります。esbuildは一般的なCSS構文を理解し、いくつかのCSSルールを理解できます(CSSファイルをバンドルしてCSSを合理的にミニファイするのに十分です)。しかし、esbuildはCSSに関する完全な知識を持っていません。これは、esbuildがCSSに対して「ゴミが入ればゴミが出る」という哲学を採用していることを意味します。コンパイルされたCSSにタイプミスがないことを確認するには、esbuildに加えてCSSリンターを使用する必要があります。

@importの順序はブラウザと一致

CSSの@importルールは、JavaScriptのimportキーワードとは動作が異なります。JavaScriptでは、importはほぼ「インポートされたファイルがこのファイルが評価される前に評価されるようにする」という意味ですが、CSSでは、@importはほぼ「ここでインポートされたファイルを再度評価する」という意味です。例えば、以下のファイルがあるとします。

JavaScriptの直感から、このコードは最初に背景を白、テキストを黒にリセットし、その後、背景を黒、テキストを白に上書きすると考えるかもしれません。これは起こりません。代わりに、本文は完全に黒になります(前景と背景の両方)。これは、@importがインポートされたファイルによってインポートルールが置き換えられたかのように動作する(C/C++の#includeのような)ためで、ブラウザは以下のコードを見ることになります。

/* reset.css */
body {
  color: black;
  background: white;
}

/* foreground.css */
body {
  color: white;
}

/* reset.css */
body {
  color: black;
  background: white;
}

/* background.css */
body {
  background: black;
}

最終的にはこれになります。

body {
  color: black;
  background: black;
}

この動作は残念ですが、esbuildはこのように動作するのは、それがCSSの仕様であり、ブラウザでのCSSの動作方法だからです。これは、postcss-importなどの他の一般的に使用されるCSS処理ツールの中には、JavaScriptの順序ではなくCSSの順序でCSSインポートを正しく解決しないものがあるため、重要です。これらのツール用に記述されたCSSコードをesbuildに移植する場合(またはブラウザでCSSコードをネイティブに実行するように切り替える場合でも)、コードが正しくないインポート順序に依存している場合、外観が変更される可能性があります。

テキスト

ローダー: text

このローダーは、.txtファイルに対してデフォルトで有効になっています。ビルド時にファイルを文字列として読み込み、その文字列をデフォルトエクスポートとしてエクスポートします。使用方法は次のようになります。

import string from './example.txt'
console.log(string)

バイナリ

ローダー: binary

このローダーは、ビルド時にファイルをバイナリバッファとして読み込み、Base64エンコーディングを使用してバンドルに埋め込みます。ファイルの元のバイトは実行時にBase64からデコードされ、デフォルトエクスポートとしてUint8Arrayとしてエクスポートされます。使用方法は次のようになります。

import uint8array from './example.data'
console.log(uint8array)

ArrayBufferが必要な場合は、uint8array.bufferにアクセスするだけです。このローダーはデフォルトでは有効になっていません。適切なファイル拡張子に対して次のように設定する必要があります。

CLI JS Go
esbuild app.js --bundle --loader:.data=binary
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  loader: { '.data': 'binary' },
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    Loader: map[string]api.Loader{
      ".data": api.LoaderBinary,
    },
    Write: true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

Base64

ローダー: base64

このローダーは、ビルド時にファイルをバイナリバッファとして読み込み、Base64エンコーディングを使用して文字列としてバンドルに埋め込みます。この文字列は、デフォルトエクスポートを使用してエクスポートされます。使用方法は次のようになります。

import base64string from './example.data'
console.log(base64string)

このローダーはデフォルトでは有効になっていません。適切なファイル拡張子に対して次のように設定する必要があります。

CLI JS Go
esbuild app.js --bundle --loader:.data=base64
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  loader: { '.data': 'base64' },
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    Loader: map[string]api.Loader{
      ".data": api.LoaderBase64,
    },
    Write: true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

これをUint8ArrayまたはArrayBufferに変換する場合は、代わりにbinaryローダーを使用する必要があります。これは、通常のatob変換プロセスよりも高速な最適化されたBase64からバイナリへのコンバーターを使用します。

データURL

ローダー: dataurl

このローダーは、ビルド時にファイルをバイナリバッファとして読み込み、Base64エンコードされたデータURLとしてバンドルに埋め込みます。この文字列は、デフォルトエクスポートを使用してエクスポートされます。使用方法は次のようになります。

import url from './example.png'
let image = new Image
image.src = url
document.body.appendChild(image)

データURLには、ファイル拡張子と/またはファイルの内容に基づいてMIMEタイプの最適な推測が含まれており、バイナリデータの場合、次のようなものになります。

data:image/png;base64,iVBORw0KGgo=

…またはテキストデータの場合、次のようなものになります。

data:image/svg+xml,<svg></svg>%0A

このローダーはデフォルトでは有効になっていません。適切なファイル拡張子に対して次のように設定する必要があります。

CLI JS Go
esbuild app.js --bundle --loader:.png=dataurl
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  loader: { '.png': 'dataurl' },
  outfile: 'out.js',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    Loader: map[string]api.Loader{
      ".png": api.LoaderDataURL,
    },
    Write: true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

外部ファイル

目的の動作に応じて、外部ファイルに使用できる2つの異なるローダーがあります。両方のローダーを以下に説明します。

fileローダー

ローダー: file

このローダーは、ファイルをアウトプットディレクトリにコピーし、ファイル名を文字列としてバンドルに埋め込みます。この文字列は、デフォルトエクスポートを使用してエクスポートされます。使用方法は次のようになります。

import url from './example.png'
let image = new Image
image.src = url
document.body.appendChild(image)

この動作は、Webpackのfile-loaderパッケージと意図的に似ています。このローダーはデフォルトでは有効になっていません。適切なファイル拡張子に対して次のように設定する必要があります。

CLI JS Go
esbuild app.js --bundle --loader:.png=file --outdir=out
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  loader: { '.png': 'file' },
  outdir: 'out',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    Loader: map[string]api.Loader{
      ".png": api.LoaderFile,
    },
    Outdir: "out",
    Write:  true,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

デフォルトでは、エクスポートされた文字列はファイル名だけです。エクスポートされた文字列にベースパスをプリペンドする場合は、公開パスAPIオプションを使用できます。

copyローダー

ローダー: copy

このローダーはファイルをアウトプットディレクトリにコピーし、コピーされたファイルを指すようにインポートパスを書き換えます。つまり、最終的なバンドルにはインポートは依然として存在し、最終的なバンドルはファイルを含める代わりにファイルを依然として参照します。これは、esbuildの出力に追加のバンドルツールを実行する場合、まれに使用されるデータファイルをバンドルから省略して起動パフォーマンスを向上させる場合、またはインポートによってトリガーされるランタイムの特定の動作に依存する場合に役立つ可能性があります。例えば

import json from './example.json' assert { type: 'json' }
console.log(json)

次のコマンドで上記のコードをバンドルする場合

CLI JS Go
esbuild app.js --bundle --loader:.json=copy --outdir=out --format=esm
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  loader: { '.json': 'copy' },
  outdir: 'out',
  format: 'esm',
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    Loader: map[string]api.Loader{
      ".json": api.LoaderCopy,
    },
    Outdir: "out",
    Write:  true,
    Format: api.FormatESModule,
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

結果のout/app.jsファイルは次のようになります。

// app.js
import json from "./example-PVCBWCM4.json" assert { type: "json" };
console.log(json);

インポートパスがコピーされたファイルout/example-PVCBWCM4.jsonを指すように書き換えられ(アセット名設定のデフォルト値によりコンテンツハッシュが追加されています)、JSONのインポートアサーションが保持されているため、ランタイムがJSONファイルをロードできることに注意してください。

空ファイル

ローダー: empty

このローダーは、esbuildにファイルを空であると見なすように指示します。特定の状況でバンドルからコンテンツを削除するのに役立つ方法です。たとえば、.cssファイルをemptyでロードするように設定して、JavaScriptファイルにインポートされたCSSファイルをesbuildがバンドルするのを防ぐことができます。

CLI JS Go
esbuild app.js --bundle --loader:.css=empty
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  loader: { '.css': 'empty' },
})
package main

import "github.com/evanw/esbuild/pkg/api"
import "os"

func main() {
  result := api.Build(api.BuildOptions{
    EntryPoints: []string{"app.js"},
    Bundle:      true,
    Loader: map[string]api.Loader{
      ".css": api.LoaderEmpty,
    },
  })

  if len(result.Errors) > 0 {
    os.Exit(1)
  }
}

このローダーを使用すると、CSSファイルからインポートされたアセットを削除することもできます。たとえば、.pngファイルをemptyでロードするように設定すると、url(image.png)などのCSSコード内の.pngファイルへの参照がurl()に置き換えられます。