class: center, middle # `esbuild`用のプラグインを開発した話 2024-12-18 Kyoto.js id:Windymelt --- # Windymelt - [Windymelt](https://www.3qe.us/)です - 全宇宙でこのIDを使っています - mixi2もやってるよ - GitHub, X/Twitter, etc... - PGP: `F2FC 63C2 42C0 4D9D` - 2016- [株式会社はてな](https://hatena.co.jp/) - [AIを利用した発話分析ソリューションtoitta](https://toitta.com/)の開発 - プライベート - [動画生成ツールZMM](https://www.3qe.us/zmm/doc) - Scalaの記事たくさん[書いてる](https://blog.3qe.us/category/scala) - インターネットが大好き --- # Windymelt
- [PR]プログラミング言語コミュニティ『Scalaわいわいランド』主宰 - ↑ググって〜 --- class: center, middle # esbuildの話をします --- # [esbuild](https://esbuild.github.io) - Web用のバンドラ - JS/TS/CSSなどに対応 - 速いのがウリ - Go製 - **プラグインで拡張可能** --- # esbuildのプラグイン - けっこう簡単に作れる - `setup`の中でイベントハンドラを仕掛けていく感じ - `name`と`setup`だけを持っている必要がある - それ以外を持っているとエラーにされる・・・ ```js const plugin = { name: 'foobarPlugin', setup: (build: Build) => { ... }, }; await esbuild.build({ entryPoints: ['main.js'], bundle: true, platform: 'node', outfile: 'dist.cjs', minify: true, plugins: [plugin], }) ``` --- # 自作する - esbuild自体が高速で面白そうだった - プラグインを自作して遊んでみたい年頃だった --- # 自作する - esbuild自体が高速で面白そうだった - プラグインを自作して遊んでみたい年頃だった - そういえば**Scala.js**というやつがあって・・・ - またか! --- # Scala.js - ScalaがJSにトランスパイルされるAltJS - Scala風言語ではなく、ほぼ全てのセマンティクスを維持する - 最近WASMに対応した(まあまあ速い) - 起動が爆速なのでFaaS向けに注目されている ```scala //> using scala 3.6.2 //> using platform js println((1 to 10).map(_ * 2).mkString("[", ",", "]")) ``` ```sh λ scala-cli --power package -o code.js code.scala.sc Wrote /home/windymelt/temp/scalajs-exercise/code.js, run it with node ./code.js λ node ./code.js [2,4,6,8,10,12,14,16,18,20] ``` --- # Scala.jsをesbuildでバンドルしたい - 先行事例 - `sbt`(Scalaのビルドツール)プラグインとして実装されているやつがある - [ptrdom/scalajs-esbuild](https://github.com/ptrdom/scalajs-esbuild) - 俺がやりたい - `esbuild`側のプラグインとしてシームレスに動作させたい - 最終的にjsが欲しいのだからバンドラ側のプラグインとして作ったほうが筋が良い - [windymelt/esbuild-plugin-scalajs](https://github.com/windymelt/esbuild-plugin-scalajs) - [![NPM Version](https://img.shields.io/npm/v/esbuild-scalajs)](https://www.npmjs.com/package/esbuild-scalajs) - minifyとかを細かくいじれるので嬉しい --- # DEMO - `bat src/main/scala/scalamain/ScalaMain.scala` - `bat src/main/js/main.js` - `sbtn` -- `clean` - `bat esbuild.mjs` - `bat package.json` - `pnpm run build` - `node ./dist.cjs` - `ls -lah dist.cjs` --- # esbuildプラグインのメンタルモデル - プラグインが登録されると、esbuildは各プラグインに対して`setup`を呼び出し、ビルド情報を渡す - プラグインは、ビルド情報に対してイベントハンドラを設定する --- # esbuildプラグインのメンタルモデル - よく使うイベントハンドラ(とパラメータ) - `build.onResolve({ pattern }, (args) => { ... })` - 「このregexに対応するjsファイルはこれ」というマッピングをプラグインが知っているということを教える --- # esbuildプラグインのメンタルモデル - `import from ほげ`されると`onResolve`で設定した`pattern`にマッチするかどうかが検査されていく - 最初にマッチしたプラグインにresolveが一任される --- # 今回の作戦 - `import foo from 'scala:bar'` という記法で起動する - Scala.jsの場合一定の場所にモジュールが吐かれるのでこの場所を教えてやればよい ``` ./target/scala-${version}/${project}-${optimizeLevel}/${moduleName}.${extension} // 例 ./target/scala-3.5.2/esbuild-plugin-for-scala-js-fastopt/esbuildScalaJsPlugin.js ``` --- # 今回の作戦 - Scala.jsの出力自体は`sbt`に任せる - ユーザは既にそのセットアップをしているという前提にする - `sbt`の起動はくそ遅い - 最新兵器、`sbtn`を使う --- # `sbtn` - ネイティブコンパイルされた`sbt`クライアント - 初回起動時は本体の`sbt`を起動し、その後はLSP on domain socketで通信する - 爆速で起動するのでピッタリ - より速度を出すには直接LSP叩くことになる --- # 今回の作戦 - `onResolve`のハンドラにひっかかったらとりあえず`sbtn`を起動し、終わったらパスを返す - 富豪的だが`sbtn`はビルド済みだとすぐ終了するのでこれでいい --- class: center, middle # 実装上のおもしろポイント --- # プラグイン自体がScala.jsで書かれている ```scala // 抜粋 inline def runBuild(isProd: Boolean): Future[Unit] = { // assuming sbtn val subcommand = if isProd then "fullLinkJS" else "fastLinkJS" scribe.info("running sbtn") runCommand("sbtn", Seq(subcommand)) } ``` - Scalaで書いてて特に困る所はなかった - Scalaの`Future`はJSの`Promise`になる - Node.jsのファイルAPIのファサードを最低限書いたりした --- # たいへんポイント - esbuild(Go)と自分のプラグイン(Scala.js)とのinteropがうまくいかない箇所があった - 具体的には[`Symbol`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Symbol)を使っていたオブジェクトの挙動がおかしくなった - Scala.jsはprivate fieldをSymbolを使って隠蔽している --- # Windymelt
- [PR]プログラミング言語コミュニティ『Scalaわいわいランド』主宰 - ↑ググって〜