TypeScript で AudioWorkletProcessor 作るなら Rollup がオススメ

こんにちは。亀山です。

皆さん日々ブラウザで遊んでいることかと思いますが、WebAudioAPI は使われていますでしょうか。すごく楽しいです。音が出る。

ところで、AudioWorkletProcessor というものがあります。WebAudioAPI のノードとして別スレッドでスクリプトを走らせられるもので、以前あった ScriptProcessorNode の改良版で、Web Worker の AudioNode 版みたいなものです。

単一の js ファイルを読みこむだけなら、従来の ScriptProcessorNode とほとんど同じマナーで簡単に作ることができますが、ちゃんとモジュールに分けて、そして TypeScript を使いたいですよね。

そんなときに Rollup がおすすめ (そして Webpack に惨敗した記録) です。

背景

最終的に必要な JavaScript のファイルは、HTML の script タグから読み込む js と、AudioWorkletProcessor に登録する js の2つです。

Webpack では entry を複数書くことで複数の js を出力することができますが、よくある webpack-dev-server ts-loader の組み合わせでやろうとすると、AudioWorkletProcessor で実行できない js が生成されてしまいました。 webpack の development の出力には、デバッグ時にエラーのオーバーレイ表示や、Hot Reload などを行う機能のための webpack のスクリプトが含まれており、これらが AudioWorkletProcessor の環境では動作しないためです。手元で色々試した結果、webpack-dev-server を使っていると、Hot Reload や overlay などを無効化してもこれらのスクリプトを除去することはできませんでした。

Web Worker 用の loader を使うように、AudioWorkletProcessor 用の loader を使うことでこの問題を回避できるようですが、これがベストというプラグインや設定はまだ無いように見えます。https://github.com/webpack/webpack/issues/11543

そんなこんなで Webpack と格闘していましたが、ただ TypeScript をトランスパイル、バンドルして js にしたいだけなのにどうしてこうなった・・・

Rollup

Rollup を使うと、余計なスクリプトが含まれない、TypeScript から素直にトランスパイルしてバンドルした js を作ることができ、rollup-plugin-serve プラグインで開発用のローカルサーバを立ち上げることができました。 Hot Reload は無いけど、どのみち AudioWorkletProcessor を再読み込みさせるためにリロードするからいいでしょう。

rollup.config.js

  • rollup.config.js でコンフィグの配列を返すとマルチエントリーにできます
    • index.js と synth.js が出力されます
  • npm で入れたライブラリを使うために @rollup/plugin-node-resolve プラグインが必要です
  • https オプションは必要なければ無くても大丈夫です。証明書は mkcert で生成できます
import commonjs from "@rollup/plugin-commonjs"
import { nodeResolve } from "@rollup/plugin-node-resolve"
import rollupTypescript from "@rollup/plugin-typescript"
import fs from "fs"
import serve from "rollup-plugin-serve"

const output = {
  dir: "public/js/",
  format: "iife",
  sourcemap: true,
}

const plugins = [
  nodeResolve({ preferBuiltins: false, browser: true }),
  commonjs(),
  rollupTypescript(),
]

export default [
  {
    input: "src/index.ts",
    output,
    plugins: [
      ...plugins,
      serve({
        contentBase: "public",
        open: true,
        https: {
          key: fs.readFileSync("./cert/localhost+1-key.pem"),
          cert: fs.readFileSync("./cert/localhost+1.pem"),
        },
      }),
    ],
  },
  {
    input: "src/synth.ts",
    output,
    plugins,
  },
]

tsconfig.json

  • ブラウザのインスペクターで ts を表示できるように sourceMap を有効にします
{
  "compilerOptions": {
    "incremental": true,
    "target": "ESNext",
    "module": "ESNext",
    "rootDir": "./src",
    "declaration": false,
    "inlineSourceMap": true,
    "inlineSources": true,
    "outDir": "./public/js",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "moduleResolution": "node"
  },
  "include": ["./src"]
}

まとめ

AudioWorkletProcessor を TypeScript で実装するときは、Webpack よりも Rollup を使うと環境構築が簡単でした。 私は Rollup はライブラリ開発用というイメージを持っていたので、開発用のローカルサーバが必要なときは Webpack をとりあえず入れとくという考えでした。今回 rollup-plugin-serve を使ってみて、config で迷うこともなく、余計なお世話がない感じもなかなか快適でした。

実際ある程度の規模のフロントエンド開発では React と HMR が必要になったりして Webpack を選ぶことになりそうですが、webpack.config.js との格闘に疲れた方にもぜひオススメです。

ESM でもいいじゃないか説

別にバンドルしないで tsc コマンドで ES Module を出力してブラウザ側で import してもいいんですが、使いたいライブラリが commonjs 形式だったのと、たくさんのファイルに分けていたので最終的には 1 ファイルにバンドルしたいという思いがありました。 ESM 詳しくないし慣れてるからとりあえずバンドルしちゃおっていうのもあります。