🥟

BunとNeovim

2023/09/27に公開

はじめに

Bunの1.0が発表されましたね!
自分はすでにいくつかのプロジェクトに導入したりして、とても気に入っています。

https://github.com/ryoppippi/str-fns

例えばこちらの自作プロジェクトでは、 node18 + pnpm + vitestからbunに移行したところ:

  • Packageのインストールが8秒から0.5秒に
  • vitestからbun testへの移行で5秒から0.3秒に
  • tscによる型チェックが2.5秒から1秒に

と、とても高速になりました。

またモノレポにも対応しているので、手元のプロジェクトたちを順次pnpmからbunに移行しています。

BunでペライチのCLI Toolを書く

さて、BunにはAuto Installという機能があります。

https://bun.sh/docs/runtime/autoimport

これはスクリプトの先頭にあるimport文を解析し、必要なパッケージを自動でインストールしてくれる機能です。

例えば

main.ts
import { z } from 'zod'

const schema = z.object({ unko: z.string() })
console.log(schema.parse({ unko: '💩' }))
➜ bun run main.ts
{
  unko: "💩"
}

のように、自動的にzodをfetchし実行してくれます。
node_moduleを作る必要がないので、気楽に実行できますね。

また先頭にシェバンを書くことで、./main.tsのように実行することもできます。

main.ts
#!/usr/bin/env bun
import { z } from 'zod'

const schema = z.object({ unko: z.string() })
console.log(schema.parse({ unko: '💩' }))
chmod +x main.ts
➜ ./main.ts
{
  unko: "💩"
}

Neovim + Deno LSPでBun+ペライチのCLI Toolを書く

さて、BunでCLI Toolを書くとき、問題となるのはLSPがないことです。
現状ではBunのLSPはなく、TSServerを使わないといけません。
しかしTSServerでライブラリの情報を補完するにはnode_modulesがないといけません。
つまり、ペライチで nvim main.ts とかやると、補完が効かなくて悲しい気持ちになります。
困りましたね。

...ここでDenoの出番です!
Denoには組み込みのLSPがあり、node_modulesがなくてもライブラリの情報を補完してくれます。
これをうまく使えば、BunでペライチのCLI Toolを書くことができるでしょう!

DenoのimportとBunのimportを行き来する

Denoでnpmのライブラリを使うときは、importのパスにnpm:プレフィックスをつけます。
逆にいえば、Deno LSPはnpm:がついていないimportのことは、npmのライブラリとして認識してくれません。

example.ts
import { z } from 'npm:zod'

const schema = z.object({ unko: z.string() })
console.log(schema.parse({ unko: '💩' }))

ということで、BunのimportをDenoのimportに変換する関数をLuaで書いてみましょう!

vim.api.nvim_create_user_command("BunToDeno", function()
    -- get current buffer text
    local text = vim.api.nvim_buf_get_lines(0, 0, -1, true)
    -- loop by lines
    for i, line in ipairs(text) do
        -- if the line stats with `import`
        if string.match(line, "^import") then
            -- import { hoge } from "hoge" => import { hoge } from "npm:hoge"
            -- import { hoge } from 'hoge' => import { hoge } from 'npm:hoge'
            text[i] = string.gsub(line, '"([^"]+)"', '"npm:%1"')
        end
    end
    -- set replaced text to current buffer
    vim.api.nvim_buf_set_lines(0, 0, -1, true, text)
end, {})

vim.api.nvim_create_user_command("DenoToBun", function()
    -- get current buffer text
    local text = vim.api.nvim_buf_get_lines(0, 0, -1, true)
    -- loop by lines
    for i, line in ipairs(text) do
        -- if the line stats with `import`
        if string.match(line, "^import") then
            -- import { hoge } from "npm:hoge" => import { hoge } from "hoge"
            -- import { hoge } from 'npm:hoge' => import { hoge } from 'hoge'
            text[i] = string.gsub(line, '"npm:([^"]+)"', '"%1"')
        end
    end
    -- set replaced text to current buffer
    vim.api.nvim_buf_set_lines(0, 0, -1, true, text)
end, {})

こうすることでcommand一発でBunとDenoのimportを行き来できます!
便利ですね!

bun to deno

ちなみに: Neovim以外をお使いの方のために

TSで実装したCLIを置いておきます。
Denoが入っている環境で動きます。
https://github.com/ryoppippi/toDeno-toBun

ちなみに: Denoでよくない?

ここまで読んで、「素直にDenoで書けばいいじゃん」と思った方もいるかもしれません。

example.ts
#!/usr/bin/env deno run -A
import { z } from 'npm:zod'

const schema = z.object({ unko: z.string() })
console.log(schema.parse({ unko: '💩' }))

これで全然動きますし、LSPも使えるし、現状色々安定してるしDenoの方が良さげな気もします。
ただし、使いたいライブラリ(主にnpmに転がっているもの)によっては、Denoで動かず、Bunでは動く、ということがあります。
cleyeはその一例です(まあDenoにはcliffyがありますが)

自分もDenoを毎日使っていますし、Denoの方が安定しているとは思いますが、場合によっては上記の理由によるBunが適している場合もあるため、どちらも使えるようにしておくと幸せになれるかなと考えています。

bun.lockbの中身をNeovimで確認する

Bunにはbun.lockbというLockfileがあります(package-lock.jsonのようなもの)。
これはbun installを実行すると自動で生成されます。

このlockfileのおかげで依存関係の解決が高速になります。
しかし、バイナリファイルであるため、中身を見ることができません。
これはちょっと不便ですね。
bun lock binary

実はbunにはlockfileをyarn v1形式で出力する機能があります。

https://bun.sh/docs/install/lockfile

これを使うと、lockfileの中身が読めるようになります。

bun bun.lockb

さて、この機能を使って、Neovimでlockfileを開いたら自動でyarnのlockfileに変換するようにしてみましょう。

vim.api.nvim_create_autocmd("BufReadCmd", {
    pattern = "bun.lockb",
    callback = function(ev)
        -- get the absolute path of the current file
        local path = vim.fn.expand("%:p")
        -- run command 'bun ' .. path and get stdout
        local output = vim.fn.systemlist("bun " .. path)
        -- set output to current buffer
        vim.api.nvim_buf_set_lines(0, 0, -1, true, output)
        -- set filetype to yarn.lock
        vim.opt_local.filetype = "conf"
        -- set readonly
        vim.opt_local.readonly = true
        -- set nomodifiable
        vim.opt_local.modifiable = false
    end,
})

これで、bun.lockbを開くと、自動でyarnのlockfileを生成し、一時的なBufferとして表示してくれます!

bun lock yarn

(ちなみに、VSCodeをお使いの方は、Bun for Visual Studio Codeを導入すると自動でlockfileがyarn形式に変換され表示されます。ただし表示がまあまあ遅いです。Neovimだと本当に一瞬で出てきますが...)

まとめ

楽しいBun Lifeを!

Discussion