タスクランナーを gulp.js から Bun に移行
いまどき gulp? と思うかもしれませんが、自分でもそう思っていたので書き換えました。
実はここ (IROIRO) はずっと gulp.js を使って HTMLやアセット類を生成、それをサーバーにデプロイという流れで運用してきました。(実行環境は WSL)
元々は メインサイト 用にかれこれ 10年くらい前に作ったもの。
最初は確か Grunt を使っていたのですが、どこかのタイミングで Gulp に移行して、ちょこちょこ改修しながら秘伝のスープ的に使ってきました。(この辺の経緯はもはや記憶が曖昧)
ですが流石に構成が古くなってきたのと、そもそも gulp の開発がもう何年も止まってしまっているので、結構前から移行を検討してきました。
やってること
実際に行っていることはそれほど複雑ではなく、
- CMSからの記事データの取得
- 記事データと pug からの完全に静的な HTML の生成
- アセット類 (JavaScript、css等) の生成
といった程度のもの。アプリケーション的な機能はほぼないので、記事の生成用にデータを加工している部分以外はそれほど重い処理は行っていません。
pug も今更といった感じですが、単純な HTML を書くという目的には手軽さや mixin による分割などが便利なので採用しています。
JavaScript は WebPack を使っていましたが、1、2ファイルで収まる程度の小規模なものなので実はわざわざ使わなくてもよい程度の規模。(一応、アプリ的なものでも使えるようにもなってはいます。)
SCSS も先日の書き換えで SASS としての機能はほとんど使わなくなったものの、mixin はまだちょっと使いたいかなといった程度。
といった感じで、中身も大分枯れた技術スタックだったりはします。
ちなみにデプロイは本番用にビルドした上で firebase deploy で行っているので、特にタスクには含まれていません。
移行先の検討
結構前から移行は検討していたものの、踏み出すにはイマイチ決め手に欠けるという状況が続いていました。
ちなみに移行の条件としては
- 少なくとも Gulp よりは新しいか、何かしらのメンテナンスが継続している
- 移行コストをなるべく軽く(重くなるならそれ相当の理由が欲しい)
保守性の改善が一番の目的で、パフォーマンスや開発効率の改善的な事はそこまで考慮していません。
他のタスクランナー
何か新しいタスクランナー的なものが出てきてないかチョイチョイ探していたのですが、結局今でも Grunt と Gulp ぐらいしかなさそうです。
(なんか別の言語だとあるのかもしれませんが、移行コストの事があるので探してはいません。)
Webフレームワークの導入
NextJS 辺りを導入して、全部乗っかってしまう案。
今から新規にもっと機能リッチなサイトだったりサービス的を新規に立ち上げるなら基本的にはこれなんですが、静的コンテンツがメインで、かつ既存のプロジェクトからの移行をするにはほぼ全面的に書き換えなくてはならず、コストが高くなるので抵抗がありました。
node script へ書き換え
gulp のタスクを単純な Node.js スクリプトに分解し、npm から起動する案。
gulp 自体が JavaScript なので、おそらくこれが現実的な解だろうとして考えていたのですが、gulp だとお手軽だったディレクトリーやファイルに対する一括処理がなんとなくシンドイのであまり乗り気でありませんでした。
しかし、後述の bun shell や標準ライブラリーによってこの負担が軽減されそうだったことがわかり、最終的には bun を使う形で採用することにしました。
Bun
Bun はJavaScript の実行環境で、ランタイム、バンドラー、npm 互換のパッケージマネージャーなどが一通り用意されています。
Zig という言語で開発されていて、実行速度の速さをウリにしており、TypeScript や JSX のトランスパイラを内臓してるので ts、tsx、jsx といったファイルを直接実行することができます。
インストールは
$ curl -fsSL https://bun.sh/install | bash
# または
$ npm install -g bun
を実行するだけ。あとは node コマンドの代わりに bun コマンドを利用すれば、bun のスクリプトとして実行されます。
最初に触れた通り、パッケージマネージャーやバンドラー、テストランナーなどといったツール群が標準で用意されており、HTTPサーバーや SQLite ドライバーなども標準 API として提供されているので外部のツールやライブラリーを導入しなくても一通りのことが実行可能です。
もちろん Webフレームワークや他のデータベースなど、足りない部分は外部ライブラリーやツールも利用できます。
Bun Shell
そして冒頭でも触れた通り、今回の移行の きっかけになったのがシェルを直接呼べる Bun shell という機能。
2024.1.20 にリリースされた v1.0.24 で搭載されたばかりの出来立てほやほやで、この時に 公式ブログでの紹介記事 を何かで見かけて興味を持ったのが、今回採用に至った直接のきっかけでもあります。
一部のコマンドは bun の独自実装によってマルチプラットフォームで動作する他、入出力結果を JavaScript側に取り込んだり渡したりすることが手軽に行えます。
例えば ファイルの一覧を文字列として読み込む場合は
import { $ } from 'bun';
const files = await $`ls`.text();
といった感じで非常に簡潔に書くことができます。
詳しい使い方は 公式のドキュメント を見てみてください。
機能的にはまだまだ不十分だったり、若干動作が暗転しなかったりという部分もあるんですが、なんとなく心理的に重い処理が 1行で実現できてしまうのが大分お手軽です。
そして採用
普段なら出たばかりの機能はしばらく様子見するんですが
- 元々 Bun には興味があって触るきっかけが欲しかった
- お試しの規模感としてはちょうどよい
- 標準ライブラリーが割と充実しているので思っていたよりも書き換えが楽そう
- ウリにしているだけあって動作が速い
- どうせ個人用なんだから最悪戻ればいい
といった理由で Bun を使ったスクリプト群として置き換えることにしました。
実際の移行
基本的には gulp のタスクを bun のスクリプトに分解して npm scripts から呼び出すように変更、外部ライブラリーは gulp のプラグインの形で利用していたものを、個々のパッケージから直に呼び出すように書き換えました。
ちなみに今回は TypeScript は使わず、JavaScript のままで動かしています。pug に渡す locale やオプション用のオブジェクトなどが可変なものが多く、1ショットのスクリプトなのでそこまで入り組んでいないというものあり、まずは移行することを目標にしてそのまま持ってくることにしました。
ただ書いてる途中でやっぱり補完は欲しくなったので、どっかで書き換えるかもしれません。
ここから、いくつが具体的な内容を挙げていきます。
ファイルの再帰処理とパイプ処理
おそらくこれが gulp の一番のメリットだったんじゃないかという点。
gulp.src("src/scss/**/*.scss")
.pipe(sass(options))
.pipe(gulp.dest("assets/css/"));
みたいな感じで、ディレクトリー内のファイルを再帰的に処理することができました。
ちなみに上記の例は src ディレクトリー内の scssファイルを SASS でトランスパイルして assets/css ディレクトリーに書き出すというもの。読み込み元のディレクトリー構成も維持されたまま出力されます。
これを bun で書くとこんな感じに
const glob = new Glob("**/*.scss");
for await (const srcFile of glob.scan("src/scss")) {
const result = sass.compile(`src/${srcFile}`, options);
// パスの取得
const path = path.dirname(srcFile);
// 拡張子を除いたファイル名の取得
const fileName = path.basename(srcFile, path.extname(srcFile));
// assets 配下にディレクトリ構造を維持しつつ、css ファイルとして保存
write(`assets/css/${path}/${fileName}.css`, result.css);
}
Glob を使ってファイルを反復処理しています。
この例では拡張子の変更が若干メンドウ。まあこの場合は単に正規表現で末尾を置き換えるだけでもよさそうですが。
write はBun の標準APIで用意されている関数で、保存先のディレクトリーが存在しない場合にはディレクトリーも含めて作成してくれます。
mkdir がまだ Bun Shell には未実装で、$ から呼び出そうとするとうまく動いてくれずに悩んだのですが、この関数で一発解決しました。
これを Bun Shell で ls を使ってやろうとするとこうなります。
for await (const srcFile of $`ls src/**/*.scss`.lines()) {
const result = sass.compile(srcFile, options);
const path = path.dirname(srcFile);
const fileName = path.basename(srcFile, path.extname(srcFile));
write(`${srcFile.replace(/^src/, "assets/css/")${path}/${fileName}.css`, result.css);
}
ほぼ同じですが、パスに対象ディレクトリーも含まれているので追加で置き換える必要があります。この辺りが若干面倒だったので Glob を使う形にしています。
ちなみに実際には下記のようなラッパー関数を作り
async function iterateGlob(pattern, scanPath, func) {
const glob = new Glob(pattern);
for await (const path of glob.scan(scanPath)) {
await func(path);
}
}
Glob の生成を省略できるようにしています。
await iterateGlob("**/*.scss", config.src.styles, async (srcFile) => {
// process
});
Webpack
Webpack は Bun の組み込みのバンドラーである Bun.build で置き換えました。
ただ最初の方でも書きましたが、ここのスクリプトはあまり大したことをしておらず、TypeScript すら使ってない素の JavaScript で書いてしまっています。(他の所では TypeScript なんです。昔からあるやつだけなんです。)
なのでビルドの必要もあまりなく、単に import の解決や minimize をやっている程度です。
Bun.build はまだベータということもあって機能がそこまで充実していない印象なので、複雑な構成の場合は Vite 辺りを導入した方がよさそうです。(公式サイトに サンプル があります。)
ファイル変更の監視
Gulp だと gulp.watch を使って
gulp.watch(`src/scss/**/*.@(css|scss)`, gulp.task('styles'));
といった形で書くことができます。
Bun の場合は node の fs.watch が利用できますが、Glob での指定ができないので大人しく外部ライブラリーを使用します。
gulp.watch は chokidar が使われているようですが、今回は glob-watcher というのを使ってみました。というか別のライブラリーというか単に chokidar のラッパーで、もう少し手軽に書けるようになったものです。
watch([`src/scss/**/*.{scss,css}`], function (done) {
$`bun styles`;
done();
});
$bun [script]
を使って別ファイルの bun を呼び出しています。
gulp のようなタスクの概念はないので 1タスク 1ファイルとなりますが、まあそれは大人しくファイルを分ければよいかと思います。(タスクが多くなると整理が大変そうですが)
dev server
意外なことに、というか Web専用フレームワークというわけではないので当たり前ですが、確認用の dev サーバー的なものがありそうでありません。
一応 HTTPサーバーとして Bun.serve が用意されているのですが、シンプルにリクエストに対してレスポンスを返すだけ程度の実装で、ディレクトリーを指定すれば勝手に返してくれるみたいな親切なことはしてくれません。
まあ何かライブラリーを使えばいいのですが、公式サイトにあった ファイルを読み込んでそれを返すサンプル で事足りるので、index.html の処理を加えて使うことにしました。(AI に手伝ってもらいました)
Bun.serve({
async fetch(req) {
const path = new URL(req.url).pathname;
// 拡張子が含まれていない場合は index.html を返す
if (!path.split("/").pop().includes(".")) {
return new Response(Bun.file(`${config.dest.root}${path}/index.html`));
}
// ファイル名指定が無い時に index.html を追加
const file = Bun.file(`./dist${path.endsWith("/") ? path + "index.html" : path}`);
return new Response(file);
},
});
・・・って書いてから、firebase 使ってるんだから、普通に firebaseで見れるようにすればいいじゃん と気づきました...まあ使ってないのもあるので、それ用に。(あと起動や動作も bun の方が速いです。)
同時実行
監視とdev server を同時に起動させたいとき、gulp では gulp.series と gulp.parallel でタスクの逐次処理、並列処理を行うことができました。
gulp.task("dev",
gulp.series(
"build",
gulp.parallel("watch", "serve")
)
)
上記の例では、最初に build を行ってから、watch と serve を同時に実行しています。
bun の場合は npm script で bun watch & bun serve
すれば同時起動が可能ですが、ctrl+c で停止した際にプロセスが残ってしまいます。
なので npm-run-all という外部ライブラリーを使用し
"build": "bun scripts/build.js",
"serve": "bun scripts/serve.js",
"watch": "bun scripts/watch.js",
"dev": "npm-run-all build --parallel watch serve"
という形で記載して、gulp の時と同じような動作をさせるようにしています。
というわけで、この記事が新しい仕組みで生成された最初の記事となります。
あまり複雑なことをしていなかったこともあって、最初に思ってた以上にスムーズに移行することができました。
さらに実行速度もちゃんと図ってないですが数秒程度は早くなり、Bun の実行速度の恩恵を受けることもできました。
Bun を触っての感触としては、標準で一通り揃っていて、 bun shell のような野心的な機能も追加されていて勢いは感じるのですが、なんとなくまだカチッとしたシステム、業務プロジェクト等での採用は若干抵抗を感じました。
ただ、手元でちょっとした処理を行いたいような時には、一通りの機能が揃い、手軽に作成できて実行速度も速いという強みが効いてきそうなので、ひとまず個人用途でチョイチョイ触っていきたいと思いました。
まだ Gulp を使っていて移行を検討している場合は、選択肢の一つとして検討してみてはいかがでしょうか。
以上、よろしくお願いいたします。
ここ (IROIRO) のデザインを変更
ちょっと思い立って、ここのデザインを少し変更しました。
と言っても大枠ではそれほど変更は加えず、全体的な微調整とトップページを普通のブログっぽい最新の記事を表示する形にしました。
元々のコンセプトは、興味のある3つのテーマ+日常という話題にテーマ色をつけて、それぞれの記事を書いていくというものでした。 ただ、それだと単にわかりにくいだけでなく、普通のブログの方 の投稿も減ってきたので、こちらをよりブログっぽくすることにしました。
今回は一旦スタイル周りの修正ということで、実装面についてもこれから直した上で改めた触れますが、とりあえず CSSについては変更前から SCSS で一から作成しています。(リセットCSSだけ @acab/reset.css を使用)
このタイミングでCSSフレームワーク等を導入することも検討しましたが、今回はそこまで大きくデザインを変えるわけではなかったのと、(いまから導入したとしても)結局はHTML側を変えるか、CSS側を変えるかの違いにしかならないので見送りました。
その代わり、割とバラバラに書いていたスタイルを変数化して、統一感は出すようにしています。(完全にはやりきれてないですが)
というわけで、少しだけ新しくなった IROIRO をよろしくお願いいたします。
Re:Tepa を Vue から React に書き換えた
Re:Tepa は、元々 TepaEditor という名前で公開していたテキストエディターのリメイク版という位置づけのアプリ。
TepaEditor は Delphi製の Windowsネイティブアプリだったが、Re:Tepa は Webアプリを Electron によってネイティブアプリ化している。
Re:Tepa の最初のバージョンである 0.1.0 では Webアプリのフレームワークとして Vue2 を採用していたが、今回 2年半ぶりにリリースした 0.1.1 では React に変更した。
それに伴い関連するライブラリーや開発ツールなども全面的に置き換えとなった。
主な変更内容がこちら。
- Webフレームワーク:Vue (vue-cli) → React (Create React App)
- ルーティング:vue-router → React Router
- ステート管理:Vuex → Recoil (+Recoil Nexus)
- i18n:vue-i18n → react-i18next
- CSSトランジション、アニメーション:Vue → React Transition Group
- CSS:Vue(sass) → styled-components
- 画面分割:vue-resize-split-pane → Allotment
- Electron ビルド:electron-builder (vue-cli-plugin-electron-builder) → Electron Forge
- e2eテスト:Spectron → Playwright
以下は元のままで最新版に更新。
- ネイティブアプリ化:Electron
- エディターエンジン:Monaco Editor
- ユニットテスト:Jest
- リンター:eslint
- コード整形:Prettier
基本的には Vue で使っていたライブラリーを React で実現するためのライブラリーに置き換えている。CSS、アニメーション周りは Vue 自体の機能 (+WebPack) として提供されていたが、React については相当するものがないので外部のライブラリーを新たに追加した。
ステート管理には Facebook が開発している Recoil を採用。まだベータ段階だが、扱いが Redux や元の Vuex と比べても非常に楽なので使ってみたいと思い、先行して採用することにした。
ちなみに Recoil は基本 hooks のインターフェイスしか持っていないため、React コンポーネント外からの値の更新ができない。そこで Recoil Nexus というライブラリーを使って、React の外側からでも更新ができるようにした。Recoil Nexus は中身的には Recoil の useRecoilCallback で提供される get, set メソッドを無理やり外に出して (一旦空コンポーネントから hook を呼び出して内部で保持している) 参照できるようにしているもので、ちょっと裏技感があるなと思いつつ、今のところは問題なく動作しているので様子を見つつ使っている。
e2e テストやビルドツールの変更については別の機会に。
そしてこちらが Re:Tepa の構成図。(ちょっと小さくて見づらいが...)
基本的には React の部分が Vue から置き換わったのだが、それ以外の部分もごちゃごちゃして役割分担が曖昧になっていたので、Reactへの書き換えに合わせて整理して React → Commands → Searvices(Operators) → Recoil → React という流れの完全な単方向データフローになるように修正した。(むしろそっちの方が時間がかかった。) ただ、Monaco Editor がちょっと他の Reactコンポーネントからは独立した位置づけにあるので、若干矢印がややこしくなっている。
Service の中に Operator というのがあるのが若干冗長に見えるかかもしれないが、ちゃんと理由がある...のだが、この点についてもまた別の機会に。
Action というのは、元々は Delphi のライブライリー(VCL)にあった TAction というクラスを模したもの。1つのアクションを複数のコンポーネントに紐づけることで、共通のステートやメソッドを扱えるという便利なもので、Vue の時はプラグインの形で実現していた。React の場合は hooks とステートで同じような事ができてしまうので、カスタムフックと Recoil を組み合わせて Action に近いことを実現している。
書き換えの理由
Vue から React に乗り換えた最大の理由はモチベーション。
元々は Vue 最高! React イヤだ! JSX キモチワルイ!くらいの Vue派だったのですが、業務で本格的に React を触るようになって React 意外と悪くない!hooks 便利!JSX はやっぱちょっと...くらいに思うようになっていった。
加えてなかなかリリースされない Vue 3 (ようやくされたけど) 、そしてそこで採用予定の Composition API の直感的ではない点に嫌気が差し、Vue と React の立場が完全に逆転。
そしてしばらく放置状態になってしまった Re:Tepa を Vue で書かれたままにしておいたら、いよいよもってこのまま触らなくなってしまうと危機感を感じだし、最初のリリースから 1年経った 2021年末ごろから書き直しに着手することにした。
その後も仕事とゲームのの合間を縫ってちょこちょこ書き直していき、ようやく元の状態にまで持ってくることができた。
まあ React もあとどのくらい持つのか、という話もあるが、少なくとも突然消えて他に移り変わることはない...かもしれない...(し、そうなったら何で書いてても一緒)ので、とりあえずはこれを機にペースアップしてければと思う次第である。
また、開発の中でいろいろと得られた知見も多いので、それらも徐々にまとめで出せていければと考えている。
以上、よろしくお願いいたします。