Open27

Bun に入門する

きむそんきむそん

ローカルでの環境構築

きむそんきむそん
$ curl -fsSL https://bun.sh/install | bash

シェルを起動しなおして

$ bun --help
	-h, --help                      	Display this help and exit.
	-b, --bun                       	Force a script or package to use Bun's runtime instead of Node.js (via symlinking node)
	    --cwd <STR>                 	Absolute path to resolve files & entry points from. This just changes the process' cwd.
	-c, --config <PATH>?            	Config file to load Bun from (e.g. -c bunfig.toml
	    --extension-order <STR>...  	Defaults to: .tsx,.ts,.jsx,.js,.json
	    --jsx-factory <STR>         	Changes the function called when compiling JSX elements using the classic JSX runtime
	    --jsx-fragment <STR>        	Changes the function called when compiling JSX fragments
	    --jsx-import-source <STR>   	Declares the module specifier to be used for importing the jsx and jsxs factory functions. Default: "react"
	    --jsx-runtime <STR>         	"automatic" (default) or "classic"
	-r, --preload <STR>...          	Import a module before other modules are loaded
	    --main-fields <STR>...      	Main fields to lookup in package.json. Defaults to --target dependent
	    --no-summary                	Don't print a summary (when generating .bun)
	-v, --version                   	Print version and exit
	    --revision                  	Print version with revision and exit
	    --tsconfig-override <STR>   	Load tsconfig from path instead of cwd/tsconfig.json
	-d, --define <STR>...           	Substitute K:V while parsing, e.g. --define process.env.NODE_ENV:"development". Values are parsed as JSON.
	-e, --external <STR>...         	Exclude module from transpilation (can use * wildcards). ex: -e react
	-l, --loader <STR>...           	Parse files with .ext:loader, e.g. --loader .js:jsx. Valid loaders: js, jsx, ts, tsx, json, toml, text, file, wasm, napi
	-u, --origin <STR>              	Rewrite import URLs to start with --origin. Default: ""
	-p, --port <STR>                	Port to serve Bun's dev server on. Default: "3000"
	    --smol                      	Use less memory, but run garbage collection more often
	    --minify                    	Minify (experimental)
	    --minify-syntax             	Minify syntax and inline data (experimental)
	    --minify-whitespace         	Minify whitespace (experimental)
	    --minify-identifiers        	Minify identifiers
	    --no-macros                 	Disable macros from being executed in the bundler, transpiler and runtime
	    --target <STR>              	The intended execution environment for the bundle. "browser", "bun" or "node"
	    --inspect <STR>?            	Activate Bun's Debugger
	    --inspect-wait <STR>?       	Activate Bun's Debugger, wait for a connection before executing
	    --inspect-brk <STR>?        	Activate Bun's Debugger, set breakpoint on first line of code and wait
	    --hot                       	Enable auto reload in the Bun runtime, test runner, or bundler
	    --watch                     	Automatically restart the process on file change
	    --no-install                	Disable auto install in the Bun runtime
	-i                              	Automatically install dependencies and use global cache in Bun's runtime, equivalent to --install=fallback
	    --install <STR>             	Install dependencies automatically when no node_modules are present, default: "auto". "force" to ignore node_modules, fallback to install any missing
	    --prefer-offline            	Skip staleness checks for packages in the Bun runtime and resolve from disk
	    --prefer-latest             	Use the latest matching versions of packages in the Bun runtime, always checking npm
	    --silent                    	Don't repeat the command for bun run
	    --dump-environment-variables	Dump environment variables from .env and process as JSON and quit. Useful for debugging
	    --dump-limits               	Dump system limits. Useful for debugging

-------

Bun: a fast JavaScript runtime, package manager, bundler and test runner. (1.0.3)

  run       ./my-script.ts       Run JavaScript with Bun, a package.json script, or a bin
  test                           Run unit tests with Bun
  x         prettier             Install and execute a package bin (bunx)
  repl                           Start a REPL session with Bun

  init                           Start an empty Bun project from a blank template
  create    elysia               Create a new project from a template (bun c)

  install                        Install dependencies for a package.json (bun i)
  add       zod                  Add a dependency to package.json (bun a)
  remove    backbone             Remove a dependency from package.json (bun rm)
  update    tailwindcss          Update outdated dependencies
  link                           Link an npm package globally
  unlink                         Globally unlink an npm package
  pm                             More commands for managing packages

  build     ./a.ts ./b.jsx       Bundle TypeScript & JavaScript into a single file

  upgrade                        Get the latest version of Bun
  bun --help                     Show all supported flags and commands

  Learn more about Bun:          https://bun.sh/docs
  Join our Discord community:    https://bun.sh/discord

動いた

きむそんきむそん

バージョンの切り替えをしたくなるけど nodenv とか nvm 相当のことをできるツールはさすがにまだないのかな

きむそんきむそん

nvm でのサポート:
https://github.com/nvm-sh/nvm/issues/3189
availableなバージョン一覧が取得できないためあまり積極的ではない

volta でのサポート:
https://github.com/volta-cli/volta/issues/1465
前向きに議論されているが、Windowsのサポートがないことを理由にまだ実施されていない。

https://bun.sh/blog/bun-v1.0#bun-more-thing

We'll be rapidly improving support for Windows over the coming weeks. If you're excited about Bun for Windows, we'd encourage you to join the #windows channel on our Discord for updates.

とのことで近々正式サポートが入れば volta もサポートされそう。

nodenv でのサポート:
Issueは見当たらない、そもそも nodenv より bunenv的なものができる気もする?

てことで bun 自体のバージョン切り替えをしたいなら volta を待つのが有力そう。現時点では curl -fsSL https://bun.sh/install | bash で入れてあげるか、コンテナで固定するとかしかないっぽい

きむそんきむそん

bun を対話的に動かす

ドキュメント読んでも見当たらないのでサポートないのかなと思ったら

https://github.com/jhmaster2000/bun-repl

これがあった。
特に明示的なインストールは不要で(裏でインストールしてるっぽい)

$ bun repl

すると良い

$ bun repl
Welcome to Bun v1.0.3
Type ".help" for more information.
[!] Please note that the REPL implementation is still experimental!
    Don't consider it to be representative of the stability or behavior of Bun overall.
>

動いた。書いてあるとおりまだ experimental

きむそんきむそん
➜ bun repl
Welcome to Bun v1.0.3
Type ".help" for more information.
[!] Please note that the REPL implementation is still experimental!
    Don't consider it to be representative of the stability or behavior of Bun overall.
> const x = 'hello'
undefined
> console.log(x)
hello
undefined
> const x = 'hello2'
SyntaxError: Can't create duplicate variable: 'x'

> const y: string = 'hello'
undefined
> const z: string = 10
undefined
  • 基本的な使い心地は node と同じ
  • 型注釈もパースエラーにならずかける
  • とはいえ型チェックが入るわけでは当然なく、型は書いても特に意味がない
    • 補完が入るとかも現時点ではないので本当に型付きのままでも動きはするよ、ってだけのもので(前に話題になってた tc39/proposal-type-annotations と同じイメージ)
きむそんきむそん

bun でスクリプトを実行する

きむそんきむそん

bun でなにが嬉しいって ts がそのまま動くことだよね
まずはこれから試していく

type Main = () => Promise<void>

const main: Main = async () => {
  console.log('hello! from bun')
}

main()
  .then(() => {
    console.log('successfully completed!')
  })
  .catch((err) => {
    console.error(err)
  })

package.json 等ない状態で動かしてみる

$ bun run ./sample.ts
hello! from bun
successfully completed!

正常に実行できた :tada:

きむそんきむそん

一応型チェックが暗黙的に入るのか確かめる

type Main = () => Promise<string>

const main: Main = async () => {
  console.log('hello! from bun')
}

main()
  .then(() => {
    console.log('successfully completed!')
  })
  .catch((err) => {
    console.error(err)
  })
$ bun run ./sample.ts
hello! from bun
successfully completed!

特にエラーが起きないのでやっぱり入ってない、型チェックは tsc でやって実行は bun でできるよって感じで他のバンドラと同じ

きむそんきむそん

次は同じく package がない状態でも node の API を利用できるか

import { readFileSync } from 'node:fs'

type Main = () => Promise<void>

const main: Main = async () => {
  const content = readFileSync('./sample.ts', { encoding: 'utf8' })
  console.log('hello! from bun', content)
}

main()
  .then(() => {
    console.log('successfully completed!')
  })
  .catch((err) => {
    console.error(err)
  })
$ bun run ./sample.ts
hello! from bun import { readFileSync } from 'node:fs'

type Main = () => Promise<void>

const main: Main = async () => {
  const content = readFileSync('./sample.ts', { encoding: 'utf8' })
  console.log('hello! from bun', content)
}

main()
  .then(() => {
    console.log('successfully completed!')
  })
  .catch((err) => {
    console.error(err)
  })

successfully completed!

できる

きむそんきむそん

packageManager を試す

きむそんきむそん
$ bun init
bun init helps you get started with a minimal project and tries to guess sensible defaults. Press ^C anytime to quit

package name (bun-playground): 
entry point (index.ts): src/index.ts

Done! A package.json file was saved in the current directory.
 + src/index.ts
 + .gitignore
 + tsconfig.json (for editor auto-complete)
 + README.md

To get started, run:
  bun run src/index.ts

でプロジェクト初期化用のファイル群が作られる

  • README.md
  • tsconfig.json
  • bun.lockb
    • package-lock.json 等のロックファイル相当
  • package.json
  • .gitignore

が自動生成される
他のパッケージマネージャだと package.json くらいなので良くも悪くも色々やってくれる感じ。

bun-types なるパッケージを自動でインストールしておいてくれて、tsconfig にもこれが入っている

tsconfig.json
{
  "compilerOptions": {
    "lib": ["ESNext"],
    "module": "esnext",
    "target": "esnext",
    "moduleResolution": "bundler",
    "moduleDetection": "force",
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "composite": true,
    "strict": true,
    "downlevelIteration": true,
    "skipLibCheck": true,
    "jsx": "react-jsx",
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "allowJs": true,
    "types": [
      "bun-types" // add Bun global
    ]
  }
}

types に自動で入ってきて、@types/node に相当する型等が入っているので init したらすぐ node の API を型付きで利用できる。至れり尽くせり。

きむそんきむそん
$ bun add -D typescript
bun add v1.0.3 (25e69c71)

 installed typescript@5.2.2 with binaries:
  - tsc
  - tsserver


 1 packages installed [97.00ms

パッケージ追加の仕方も他のパッケージマネージャと特に変わらず

きむそんきむそん

yarn listpnpm list 相当のコマンドは見当たらず(当然といえば当然だけど)パッケージマネージャとしてはやっぱり既存のもののほうがリッチな感じはする。併用できるかは気になる

きむそんきむそん

packageManager といえば workspace 提供の責務もあるのでこっちも見ていく

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

Bun supports workspaces in package.json. Workspaces make it easy to develop complex software as a monorepo consisting of several independent packages.

てことで対応されている
せっかくなのでモノレポ化してみる

packages/sample/package.json
{
  "name": "sample",
  "module": "index.ts",
  "main": "dist/index.js",
  "type": "module",
  "scripts": {
    "build": "run-s 'build:*'",
    "build:bundle": "bun bun ./index.ts --outdir dist",
    "build:types": "tsc"
  },
  "devDependencies": {
    "bun-types": "latest",
    "npm-run-all": "^4.1.5",
    "typescript": "^5.2.2"
  },
  "peerDependencies": {
    "typescript": "^5.0.0"
  }
}
packages/sample/tsconfig.json
{
  "compilerOptions": {
    "lib": ["ESNext"],
    "module": "esnext",
    "target": "esnext",
    "moduleResolution": "bundler",
    "moduleDetection": "force",
    "allowImportingTsExtensions": true,
    "composite": true,
    "strict": true,
    "downlevelIteration": true,
    "skipLibCheck": true,
    "jsx": "react-jsx",
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "allowJs": true,
    "types": [
      "bun-types" // add Bun global
    ],
    // added
    "outDir": "dist",
    "noEmit": false,
    "emitDeclarationOnly": true
  }
}
package.json
{
  "name": "bun-playground",
  "module": "src/index.ts",
  "type": "module",
  "workspaces": [
    "packages/*"
  ],
  "dependencies": {
    "sample": "workspace:*"
  },
  "devDependencies": {
    "bun-types": "latest",
    "typescript": "^5.2.2"
  },
  "peerDependencies": {
    "typescript": "^5.0.0"
  }
}
sample.ts
import { valueInSampleApp } from 'sample'

type Main = () => Promise<void>

const main: Main = async () => {
  console.log('hello! from bun', valueInSampleApp)
}

main()
  .then(() => {
    console.log('successfully completed!')
  })
  .catch((err) => {
    console.error(err)
  })

動かしてみる

$ bun --cwd ./packages/sample run build
sample scripts:

 bun run build
   run-s 'build:*'

 bun run build:bundle
   bun bun ./index.ts --outdir dist

 bun run build:types
   tsc

3 scripts

$ bun run ./sample.ts 
hello! from bun valueInSampleApp
successfully completed!

期待どおり。

きむそんきむそん

てことで最低限の要求は満たしてくれそう。
一方、node_modules は flat な構成になってそうだったので pnpm の

  • 厳格なパッケージ管理(暗黙依存のパッケージは参照できない)
  • 細かな作り込み (pnpm fetch や 正規表現での複数のスクリプトの同時実行等)

等は pnpm に優位がありそうで、併用できるなら bun + pnpm もありかなと思ったりした

きむそんきむそん

NestJS は使える??

古いデコレータ依存なので無理かな...

$ cd apps
$ git clone https://github.com/nestjs/typescript-starter.git nestjs-sample
$ cd nestjs-sample
$ bun run start
$ nest start
[Nest] 7413  - 09/26/2023, 1:58:12 PM     LOG [NestFactory] Starting Nest application...
[Nest] 7413  - 09/26/2023, 1:58:12 PM     LOG [InstanceLoader] AppModule dependencies initialized +7ms
[Nest] 7413  - 09/26/2023, 1:58:12 PM     LOG [RoutesResolver] AppController {/}: +10ms
[Nest] 7413  - 09/26/2023, 1:58:12 PM     LOG [RouterExplorer] Mapped {/, GET} route +1ms
[Nest] 7413  - 09/26/2023, 1:58:12 PM     LOG [NestApplication] Nest application successfully started +3ms

普通に動いた。すごい

きむそんきむそん

なんか依存するパッケージが依存する他のパッケージも全部インストールさせられてちょっとそのまま使うのは無理そうな感じだったけど一応バンドルできた

$ bun build ./src/main.ts --outdir ./out --target bun --external class-transformer/storage

実行もできるけど

メタデータが拾えなくなって型定義消えてた、これだと class-validator とかもきついかも?

きむそんきむそん

まとめ:

  • bundleしなければ動く
  • Bun.build の方はなぜか動かなかった
  • CLI は動いてバンドルもできたが、依存パッケージが依存するパッケージも直接インストールしないといけないこと・legacyDecorator のメタデータは吸い出せないのでバンドルしてから動かす、は厳しい
きむそんきむそん

現時点でのまとめ

  • ローカルでの bun 自体のバージョン管理はまだ実用的なやり方がなさそうだが、bun で Windows サポートが入り次第 Volta がサポートしそう
  • .ts がネイティブで動くのがかなり嬉しくてちょっとしたスクリプト(例えば build 用に esbuild のAPIを叩くとか)を mjs で書いていたのが .ts でかけるようになる
  • packageManager としては workspace のサポートもあるし必要十分
    • +αがほしいときは pnpm 併用したい
  • repl も experimental でついてるがこっちは別に node に対して優れてるとかはない
  • bundle は beta 歌ってる通りまだ実用って感じはしない
  • bundle しなければ legacyDecorator を使う NestJS もちゃんと動いており、BE/FE問わず試験的に本番でもう使える感触だった