[[Obsidian Sample Plugin]]は[[Node.js]]を前提とした構成になっている。最近は[[Bun]]で開発することが多くなり、[[🦉Carnelian]]も[[Bun]]で開発しているため、[[Bun]]に移行できるかチャレンジしてみる。
> [!hint]
> 手順そのものと結論だけ知りたい場合は [[📕ObsidianのNode.jsプロジェクトをBunに移行する方法]] の参照をオススメする
## 前提条件
### 対象プラグイン
- [[🦉Another Quick Switcher]]
### 参考プロダクト
<div class="link-card">
<div class="link-card-header">
<img src="https://publish-01.obsidian.md/access/35d05cd1bf5cc500e11cc8ba57daaf88/favicon-64.png" class="link-card-site-icon"/>
<span class="link-card-site-name">minerva.mamansoft.net</span>
</div>
<div class="link-card-body">
<div class="link-card-content">
<p class="link-card-title">🦉Carnelian</p>
<p class="link-card-description">📕tadashi-aikawa専用のObsidianプラグイン。Bun製。</p>
</div>
<img src="https://publish-01.obsidian.md/access/35d05cd1bf5cc500e11cc8ba57daaf88/Notes/attachments/minerva-image.webp" class="link-card-image" />
</div>
<a class="internal-link" data-href="🦉My Products/🦉Carnelian.md"></a>
</div>
%%[[🦉Carnelian]]%%
### 環境
- [[Windows 11]]
- [[Obsidian]] 1.5.12
- [[Bun]] 1.1.3
## 開始前の所感
- 開発・実行まではできそう
- リリースフローが初めてなので躓きポイントありそう
## ファイル設定整理・依存関係更新
### package-lock.jsonとnode_modulesの削除
```console
rm -rf package-lock.json node_modules
```
### tsconfig.jsonの修正
`types`に`@types/bun`を追加する。
```json
{
"compilerOptions": {
"types": ["@types/bun"]
},
}
```
### package.jsonの修正
`scripts`のコマンド名を変更する。変更前が以下。
```json
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest-beta.json manifest.json versions.json",
"test": "jest",
"prepare": "husky install",
"pre:push": "tsc -noEmit -skipLibCheck && npm run test"
},
```
変更後。
```json
"scripts": {
"dev": "bun esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && bun esbuild.config.mjs production",
"version": "bun version-bump.mjs && git add manifest-beta.json manifest.json versions.json",
"test": "jest",
"prepare": "husky",
"pre:push": "tsc -noEmit -skipLibCheck && npm run test"
},
```
`prepare`の内容が変わったのは[[husky]]のインストール方法がv9から変わったことによるもの。[[husky]]も最新にしておく。
```console
bun i -D husky@latest
```
> [[huskyでプッシュ前に自動でTypeScriptの型チェックとテスト実行 (Bun)]]
### Bunの型定義インストール
```console
bun add -D @types/bun
```
### Taskfile変更
現状以下のようになっている。
```yaml
tasks:
build:dev: npm run dev
watch: watchexec --no-vcs-ignore --exts "js,json,css" cp main.js styles.css manifest.json /mnt/c/Users/syoum/work/minerva/.obsidian/plugins/obsidian-another-quick-switcher/
```
[[Bun]]に変える。
```yaml
build:dev: bun dev
build: bun build
test: bun test
watch: watchexec --no-vcs-ignore --exts "js,json,css" cp main.js styles.css manifest.json /mnt/c/Users/syoum/work/minerva/.obsidian/plugins/obsidian-another-quick-switcher/
```
## 動作確認
```console
task dev
```
問題なく動いてそう。
## Taskfileのdevコマンドをやめる
[[Bun]]移行とは直接関係ないが、[[GitHub Actions]]のCIを変更するにあたり、[[Taskfile]]との依存も極力切っておきたい。([[Taskfile]]はローカルでのリリース作業程度にしたい)
現状こうなっているが、[[watchexec]]に依存しない構成をつくる。
```yaml
tasks:
build:dev: npm run dev
watch: watchexec --no-vcs-ignore --exts "js,json,css" cp main.js styles.css manifest.json /mnt/c/Users/syoum/work/minerva/.obsidian/plugins/obsidian-another-quick-switcher/
dev:
desc: Build and copy files when they are updated.
deps:
- build:dev
- watch
```
監視用に[[Chokidar]]をインストール。
```console
bun add -D chokidar
```
[[esbuild]]と[[esbuild-jest]]も最新化。
```console
bun i esbuild@latest esbuild-jest@latest
```
もろもろのバージョン。
```json
"@types/bun": "^1.0.12",
"builtin-modules": "^3.3.0",
"chokidar": "^3.6.0",
"esbuild": "^0.20.2",
"esbuild-jest": "^0.5.0",
"husky": "^9.0.11",
```
`esbuild.config.mjs`は最終的にこんな感じ。
```js
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import fs from "fs";
import path from "path";
import chokidar from "chokidar";
const VAULT_DIR = "/mnt/c/Users/syoum/work/minerva";
const FILES = ["main.js", "manifest.json", "styles.css"];
// ---
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = process.argv[2] === "production";
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["src/main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
const pluginDir = path.join(
VAULT_DIR,
".obsidian/plugins/obsidian-another-quick-switcher"
);
console.log(`📁 Creating ${pluginDir} (if not existed)`);
fs.mkdirSync(pluginDir, { recursive: true });
const hotreloadPath = path.join(pluginDir, ".hotreload", "");
console.log(`🌶️ Creating a ${hotreloadPath}`);
fs.writeFileSync(hotreloadPath, "");
const watcher = chokidar.watch(FILES, { persistent: true });
watcher
.on("add", (p) => {
console.log(`♨️ ${p} is added`);
fs.copyFileSync(p, path.join(pluginDir, p));
})
.on("change", (p) => {
console.log(`♨️ ${p} is changed`);
fs.copyFileSync(p, path.join(pluginDir, p));
});
}
```
> [[📝Obsidianプラグイン開発プロジェクトのesbuildを0.15から0.20にアップグレードしたらビルド時にInvalid optionエラーになる]]
これで `task dev` をしなくても `bun dev` で同等のことを実現できる。
```console
bun dev
```
## CIを通す
[[GitHub Actions]]がこのままだと通らないので修正する。
```yaml
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: arduino/setup-task@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: task ci
```
```yaml
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- run: bun ci
```
`package.json`の`scripts`にも`ci`を追加。
```json
"scripts": {
"build": "tsc -noEmit -skipLibCheck && bun esbuild.config.mjs production",
"test": "jest",
"ci": "bun install && bun run build && bun run test"
}
```
## リリース手順変更
### バージョンアップとタグ付け
[[Taskfile]]で用意していた`release`コマンドを[[Bun]]で代替する。
```yaml
release:
desc: |
Build
∟ [Ex] task release VERSION=1.2.3
∟ [Ex] task release VERSION=1.2.3-beta
deps:
- ci
cmds:
- npm version {{.VERSION}}
- task: ci
- git push --tags
- git push
preconditions:
- sh: "[ {{.VERSION}} != '' ]"
msg: "VERSION is required."
```
`bun version`は存在しない。
> [[📝Bunでnpm versionのようにpackage.jsonのバージョンをインクリメントしたい]]
ので、`package.json`の更新も含めた`version-bump.mts`を作成する。[[Bun]]になったことで[[TypeScript]]ファイル化できる。(今までは `version-bump.mjs`だった)
> [!hint]
> `esbuild.config.mjs`などのファイルも`esbuil.config.mts`にできる。
```ts
import { readFileSync, writeFileSync } from "fs";
import { exit } from "process";
function updateVersion(version: string) {
const packageJson = JSON.parse(readFileSync("package.json", "utf8"));
packageJson.version = version;
writeFileSync("package.json", JSON.stringify(packageJson, null, " "));
const manifestBeta = JSON.parse(readFileSync("manifest-beta.json", "utf8"));
manifestBeta.version = version;
writeFileSync("manifest-beta.json", JSON.stringify(manifestBeta, null, " "));
if (version.includes("beta")) {
return;
}
const manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
const { minAppVersion } = manifest;
manifest.version = version;
writeFileSync("manifest.json", JSON.stringify(manifest, null, " "));
// update versions.json with target version and minAppVersion from manifest.json
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
versions[version] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, " "));
}
const [_1, _2, version] = Bun.argv;
if (!version) {
console.error("Required: ${version} (ex: bun version 1.2.3)");
exit(1);
}
if (!Boolean(version.match(/\d+\.\d+\.\d+/))) {
console.error("The version is not valid (ex: bun version 1.2.3)");
exit(1);
}
updateVersion(version);
```
これを`bun release`で呼び出しつつタグの更新、コミット、プッシュまで一貫して行う。
```json
"scripts": {
"dev": "bun esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && bun esbuild.config.mjs production",
"test": "jest",
"ci": "bun install && bun run build && bun run test",
"release": "bun ci && bun version-bump.mts ${VERSION} && git add package.json manifest-beta.json manifest.json versions.json && git commit -m ${VERSION} && git tag ${VERSION} && git push --tags && git push",
}
```
### GitHub Actionsの変更
タグがつけられたあとに実行されるリリース処理。
[[GitHub Actions]]の記載は変更前は以下。
```yaml
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm install
- run: npm run build
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
draft: true
files: |
main.js
styles.css
manifest.json
manifest-beta.json
```
これを変更する。
> [!caution] #2024/05/26 追記
> `permissions`を設定しないと403エラーになる場合があるので注意。
> ```yaml
> permissions:
> contents: write
> ```
```yaml
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- run: bun ci
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v2
with:
draft: true
files: |
main.js
styles.css
manifest.json
manifest-beta.json
```
## リリースしてみる
```console
VERSION=11.2.4 bun release
```
## Prettier -> Biome
[[Bun]]移行とは直接関係ないが、せっかくなので[[Prettier]]を[[Biome]]に移行する。
```console
bun remove prettier
bun add --dev --exact @biomejs/biome
bun x @biomejs/biome init
```
とりあえず `src` 配下の `js` と `ts` を一括でフォーマット。
```console
bun biome format --write src/**/*.[jt]s
```
他にも必要に応じて実行しておく。`--write`を外すと編集はされないので、不安なら結果を確認しておくのもいいかも。(個人的には必要ないと思っている)
## BiomeのLinter設定
今まで[[リンター]]を使用してこなかったが、[[Biome]]にしたので利用してみる。
デフォルトの設定は厳しすぎるので少し設定をoffにする。
```json
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"noNonNullAssertion": "off"
},
"suspicious": {
"noExplicitAny": "off"
}
}
},
```
エラーが100以上出てきたのでなかなか大変だった。。。
> [!negative] あとで気づいたけど `--apply` オプションつければ安全に修正できるところは自動で修正できた...
## package.jsonのチェックコマンド強化
`ci`コマンドと`pre:push`コマンドに[[Biome]]のコマンドを追加する。
```json
"scripts": {
"ci": "bun install && biome check src && bun run build && bun run test",
"pre:push": "tsc -noEmit -skipLibCheck && biome check src && bun run test"
}
```
`biome check`は`biome lint`と`biome format`、およびimport順の最適化を行ってくれる。
共通化できそうな気もするのが微妙に要件が違う気もするので一旦別で。
## Jestの廃止はしない
[[🦉Another Quick Switcher]]のテストは[[template literalを使った.each (Jest)|template literalを使った.each]]が使われているが、[[Bun]]では v1.1.3時点でその書き方に対応しておらず、書き直しが必要なため。また、現時点でテストの実行速度は1秒であり、`bun test`にする強いモチベーションがない。(依存関係がシンプルになるのは魅力だけど...)