まえがき
最近 esbuild を使っていくつかプロダクトを作ったときに得た知見をまとめようと思います。
執筆時点では多くの機能がプラグイン頼りになっています。これらについての情報などがあるため、公式ドキュメントの Deno instead of node を一読することをおすすめします。
Import Map について
Deno ではブラウザ互換のインポート (import
文を使ったインポート; いわゆる ESM インポート) が可能で、以下のようなインポートをすることが可能です。
import * as lodash from 'https://deno.land/x/lodash@4.17.19/lodash.js';
これに加えて、 import map にも対応しています。次に示すように import map を用意することでモジュール名を指定してインポートする (エイリアスを用意する) ことができます。型注釈のインポートにも利用できます。
{
"imports": {
"lodash": "https://deno.land/x/lodash@4.17.19/lodash.js",
"lodash/types": "npm:@types/lodash@4.17.19"
}
}
// @deno-types="lodash/types"
import * as lodash from 'lodash';
このファイルを Deno で実行する際には deno.json の importMap
プロパティにファイルパスを指定するか、import map の内容を直接記述します。
{
"importMap": "./import_map.json"
}
{
// deno.json のトップレベル
"imports": {
"lodash": "https://deno.land/x/lodash@4.17.19/lodash.js",
"lodash/types": "npm:@types/lodash@4.17.19"
}
}
または CLI オプションで次のように指定します。
$deno run --importmap import_map.json some.ts
また、vscode では deno.importmap
を指定することで補完を効かせることができます。esbuild でバンドルする方法は 外部モジュールを使う に示します。
Web ビルド最小構成
基本的には Vite などの高機能なバンドラを利用することを推奨します。
とりあえず最小の構成でビルドするサンプルです。公式ドキュメントにある通り、esbuild.stop()
を呼ぶ必要があります。実用的なテンプレートは Web ビルド用テンプレート に記述します。
import * as esbuild from 'esbuild';
import * as posix from 'posix';
await esbuild.build({
entryPoints: [posix.resolve('src', 'script.ts')],
bundle: true,
outdir: posix.resolve('dist'),
minify: true,
sourcemap: 'external',
});
esbuild.stop();
Web ビルド用テンプレート
私が実際に使用しているテンプレートです。とりあえずビルドできると思います。
ファイルの変更を監視 (watch) する
esbuild.BuildContext.watch
を呼び出すことで watch モードになります。処理は返却されるので入力待ちなどでメインプロセスを維持する必要があります。以下のソースでは Enter キーを押すことで強制再ビルドさせる機能を追加しています。q を入力したときに終了するなども可能です。デフォルトではビルド成功時には何も出力されずビルドされたかわからないためビルド結果を出力するプラグインを利用することをおすすめします (ビルド結果を出力する (プラグイン))。
const ctx = await esbuild.context(config);
await ctx.watch();
console.log('Watching...');
for await(const _ of readLines(Deno.stdin)) {
// manually rebuild
await ctx.rebuild().catch(() => {});
}
ビルドしながらサーバーをホスト (serve) する
esbuild.BuildContext.serve
を呼び出すことで HTTP サーバーをホストすることができます。上述 の watch モードと同様にメインプロセスを維持する必要があります。
const ctx = await esbuild.context(config);
const { host, port } = await ctx.serve({
servedir: config.outdir
});
console.log(`Serving on ${host}:${port}`);
for await(const _ of readLines(Deno.stdin)) {}
また、esbuild.BuildContext.watch
を続けて呼び出すことで watch モードで HTTP サーバーを建てることができます。
const ctx = await esbuild.context(config);
const { host, port } = await ctx.serve({
servedir: config.outdir
});
console.log(`Serving on ${host}:${port}`);
await ctx.watch();
console.log('Watching...');
for await(const _ of readLines(Deno.stdin)) {
// manually rebuild
await ctx.rebuild().catch(() => {});
}
JSX/TSX (React, preact など) を使う
deno.json
に次のように JSX と JSX Fragment のファクトリの設定を記述します。
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxFactory": "react.createElement",
"jsxFragmentFactory": "react.Fragment"
},
}
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxFactory": "preact.h",
"jsxFragmentFactory": "preact.Fragment"
},
}
同様の設定を esbuild に対しても行います。
import denoConfig from './deno.json' assert { type: 'json' };
const config: esbuild.BuildOptions = {
// ...
jsxFactory: denoConfig.compilerOptions.jsxFactory,
jsxFragment: denoConfig.compilerOptions.jsxFragmentFactory,
};
ビルドされる側のファイルでは次のように利用します。
/** @jsxImportSource preact */
// @deno-types="preact/types"
import * as preact from 'preact'; // この import は必須 (のはず)
const renderApp = function(target: HTMLElement) {
preact.render(<App />, target);
}
ビルド結果を出力する (プラグイン)
esbuild-plugin-result-deno
を利用します。
- https://deno.land/x/esbuild_plugin_result
- https://github.com/Tsukina-7mochi/esbuild-plugin-result-deno
import resultPlugin from 'esbuild-plugin-result';
const config: esbuild.buildOptions = {
// ...
plugins: [
buildResultPlugin(),
// and other plugins
],
};
これを利用すると次のようなビルド結果が出力されます。
23:31:06 Build succeeded in 62ms
同様のプラグインは onEnd
コールバックを利用することで簡単に実装することができます (ソース を参照)。
外部モジュールを使う
プラグインを使ってバンドルする方法
esbuild-plugin-cache-deno
を利用します。このプラグインはリモート URL のインポートを解決・キャッシュするほか、importmap に基づいてモジュール名を解決する機能があります。
- https://deno.land/x/esbuild_plugin_cache_deno
- https://github.com/Tsukina-7mochi/esbuild-plugin-cache-deno
import esbuildCachePlugin from 'esbuild-plugin-cache';
import importmap from './import_map.json' assert { type: 'json' };
const config: esbuild.BuildOptions = {
// ...
plugins: [
esbuildCachePlugin({
directory: posix.resolve('cache'),
importmap,
}),
],
};
外部モジュールのままにする方法
ビルドオプションの external
フィールドを設定することで外部ファイルとして設定することができ、読み込み時に解決するようになります (公式ドキュメント)。
import esbuildCachePlugin from 'esbuild-cache-plugin';
import importmap from './import_map.json' assert { type: 'json' };
const config: esbuild.BuildOptions = {
// ...
external: ['https://esm.sh/preact*']
};
ファイルをコピーする
copy loader を使う方法
loader として copy
を指定することでファイルをコピーすることができます。
const config: esbuild.BuildOptions = {
// ...
loader: {
'.png': 'copy',
}
};
プラグインを使う方法
esbuild-plugin-copy-deno
を使います。
import copyPlugin from 'esbuild-plugin-sass';
import importmap from './import_map.json' assert { type: 'json' };
const config: esbuild.BuildOptions = {
// ...
plugins: [
copyPlugin({
// base directory of source files
baseDir: './src',
// base directory of destination files
baseOutDir: './dist',
// files should be copied
files: [
{ from: 'imgs/*', to: 'imgs/[name][ext]' },
{ from: 'wasm/*', to: 'wasm/[name][ext]' },
],
}),
],
};
Sass を使う
esbuild-plugin-sass-deno
を使います。
- https://deno.land/x/esbuild_plugin_sass_deno (執筆時点であまりメンテナンスされていません)
- https://github.com/Tsukina-7mochi/esbuild-plugin-sass-deno (私が fork したもの)
import sassPlugin from 'esbuild-plugin-sass';
// 単体のファイルとして出力する
const config: esbuild.BuildOptions = {
// ...
plugins: [
sassPlugin(),
],
};
// スクリプトを使ってページに埋め込む
const config: esbuild.BuildOptions = {
// ...
plugins: [
sassPlugin({ loader: 'css' }),
],
};
Deno 向けにビルドする
Deno 標準のバンドラ deno bundle
コマンドは非推奨となっており、esbuild や rollup などの利用が推奨されています。
esbuild の場合ブラウザ向けビルドや Node.js 向けビルドではインポートが CommonJS 形式になってしまうため、ビルドオプションに platform
と target
を指定します (参考ソース)。
const config: esbuild.BuildOptions = {
// ...
platform: 'neutral',
target: 'deno1',
};
GitHub Actions で linter と formatter を使う
denoland/setup-deno
を使います。deno lint
で静的解析、 deno fmt --check
で formatter によるチェックを行えます。
name: lint, check format
on: push
permissions:
contents: read
jobs:
lint-and-fmt:
runs-on: ubuntu-latest
timeout-minutes: 1
steps:
- uses: actions/checkout@v3
- name: Setup Deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- name: Lint
run: deno lint
- name: Check format
run: deno fmt --check