Slide 1

Slide 1 text

Go 製CLI ツールをnpm で配布するには syumai 『Gopher のためのCLI ツール開発』最新事情 LT @Findy TECH BATON (2025/7/28)

Slide 2

Slide 2 text

自己紹介 syumai ECMAScript 仕様輪読会 / Asakusa.go 主催 株式会社ベースマキナで管理画面のSaaS を開発中 Go でGraphQL サーバー (gqlgen) や TypeScript でフロント エンドを書いています Software Design 2023 年12 月号から2025 年2 月号まで Cloudflare Workers の連載をしました Twitter ( 現𝕏): @__syumai Website: https://syum.ai

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

npm

Slide 5

Slide 5 text

npm Node.js のpackage manager https://npmjs.com Node.js をインストールすると一緒についてくる コード内で使うpackage だけでなく、コマンドも配布可能 npm install -g {コマンド名} # グローバルにコマンドをインストール npx {コマンド名} # インストールせずにコマンドを実行 (or プロジェクト内実行) # bunx {コマンド名} / deno run npm:{コマンド名} でも使える

Slide 6

Slide 6 text

なぜGo 製のコマンドをnpm で配布するのか? ツールの対象ユーザーの範囲を広げるため 手元にNode.js はあるがGo はないというユーザーは一定数いるはず JS エコシステム向けのツールをGo で作るため 性能の問題でJS ではなくGo で作った方が有利なケース esbuild ( バンドラー) が有名

Slide 7

Slide 7 text

npm での配布方法

Slide 8

Slide 8 text

npm での配布方法 1. WebAssembly (Wasm) で配布 2. 各OS/ アーキテクチャ向けのビルド済みバイナリで配布 今回自分は、Wasm で配布しました

Slide 9

Slide 9 text

配布方法のPros / Cons Pros 1. Wasm で配布 アーキテクチャに縛られない 2. 各OS/ アーキテクチャ向けのビルド済みバイナリで配布 CGO が使える OS の機能がフルに使える 高パフォーマンス

Slide 10

Slide 10 text

配布方法のPros / Cons Cons 1. Wasm で配布 CGO が使えない 使えるOS の機能が限定される 仕組み上、比較的低パフォーマンスになりやすい goroutine がJS ランタイム上でシングルスレッドで動くなど 2. 各OS/ アーキテクチャ向けのビルド済みバイナリで配布 ビルド・配布がやや面倒 GoReleaser などで楽はできる JS / Wasm に閉じないので、optionalDependencies / postinstal script といった仕 組みを使う必要がある

Slide 11

Slide 11 text

配布方法のPros / Cons まとめ 手軽さを求めるならWasm で配布 Go のフルの性能・機能を求めるなら各OS/ アーキテクチャ向けにビルドして配布

Slide 12

Slide 12 text

esbuild の例 esbuild では、下記の2 パッケージに分けて、両方の方式をサポートしている esbuild : 2. 各OS/アーキテクチャ向けのビルド済みバイナリで配布 esbuild-wasm : 1. Wasmで配布 ただし、CLI ツールとして使えるのは esbuild の方のみ

Slide 13

Slide 13 text

https://github.com/evanw/esbuild/blob/v0.25.8/npm/esbuild/package.json

Slide 14

Slide 14 text

OS / アーキテクチャごとのoptionalDependencies が指定されている https://github.com/evanw/esbuild/blob/v0.25.8/npm/esbuild/package.json

Slide 15

Slide 15 text

実際に作った例

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

中身はこれだけ

Slide 19

Slide 19 text

xidgen をnpm に公開した手順 1. package.json を追加 2. Wasm にビルド 3. wasm_exec.js とwasm_exec_node.js を追加 4. wasm_exec_node.js を修正 5. package.json に bin の設定を追加 6. npm publish で公開

Slide 20

Slide 20 text

1. package.json を追加 Go プロジェクト内で npm init して、素朴な内容のpackage.json を追加します { "name": "xidgen", "version": "0.1.0", // ... }

Slide 21

Slide 21 text

2. Wasm にビルド GOOS=js GOARCH=wasm でビルドするコマンドをpackage.json に入れます npm run build で実行できます { "name": "xidgen", "version": "0.1.0", "scripts": { "build": "GOOS=js GOARCH=wasm go build -o ./main.wasm ." } // ... } GOOS=wasip1 GOARCH=wasm でビルドする方法もあるが、Node.js の node:wasi はま だExperimental (2025 年7 月28 日時点) https://nodejs.org/api/wasi.html

Slide 22

Slide 22 text

3. wasm_exec.js とwasm_exec_node.js を追加 以下が golang/go リポジトリに含まれている wasm_exec.js Go のWasm をブラウザ / Node.js で実行するための設定を行うスクリプト wasm_exec_node.js Go のWasm をNode.js で実行するエンドポイントのスクリプト 上記2 ファイルを取得する https://github.com/golang/go/tree/go1.24.5/lib/wasm (Go 1.24.5 の場合) または、 $GOROOT/lib/wasm から cp で取得するのも可 ( この方が楽かも)

Slide 23

Slide 23 text

4. wasm_exec_node.js を修正 元の wasm_exec_node.js は、Wasm バイナリのパスをコマンドライン引数で受け取る 設定になっている npm でpublish する場合、Wasm バイナリは同梱されるので、パスを固定する

Slide 24

Slide 24 text

4. wasm_exec_node.js を修正 修正その1 以下の引数のカウント、引数のslice 処理を直す // Wasmバイナリのパスは固定なので、引数の最低数を数える必要がない if (process.argv.length < 3) { console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); process.exit(1); } const go = new Go(); // Wasmのパスの指定が含まれないので、`2`だと除外しすぎになる // 引数を使わないコマンドなら、行ごと消してOK go.argv = process.argv.slice(2);

Slide 25

Slide 25 text

4. wasm_exec_node.js を修正 修正その2 コマンドライン引数からWasm バイナリのパスを受け取る箇所がある WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject) これを、直接指定する形に直す // Wasmバイナリはpackageに同梱されている const wasmPath = path.resolve(__dirname, "./main.wasm"); WebAssembly.instantiate(fs.readFileSync(wasmPath), go.importObject)

Slide 26

Slide 26 text

4. wasm_exec_node.js を修正 修正その3 ファイルの先頭にshebang を追加 bin に直接追加するのに使う #! /usr/bin/env node

Slide 27

Slide 27 text

5. package.json に bin を追加 wasm_exec_node.js を実行する形で設定する { "name": "xidgen", "version": "0.1.0", "scripts": // ... "bin": { "xidgen": "wasm_exec_node.js" }, // ... }

Slide 28

Slide 28 text

6. npm publish する publish に成功すると、npm install できるようになる publish 結果の例 http://npmjs.com/package/xidgen ※ *.wasm を .gitignore に入れると、( .npmignore が無いときに) npm package に 入らないので、別で .npmignore を作った方がいいです

Slide 29

Slide 29 text

実際の実装 同じ方法で、リポジトリ内の bomify コマンドをpublish してみました! https://github.com/syumai/cli/pull/1 cat {CSVファイル} | npx bomify > csv_with_bom.csv みたいに使えます

Slide 30

Slide 30 text

まとめ npm でのGo のCLI ツールの配布は、手軽さを優先するならWasm で簡単にできる Go のフル機能を使いたい場合の配布方法もある やや作り込みが必要な可能性あり npm で配布すると、JS ユーザーにもGo 製CLI ツールを使ってもらいやすくなる

Slide 31

Slide 31 text

ご清聴ありがとうございました!

Slide 32

Slide 32 text

補足 esbuild の install.js は動的に生成しているらしい https://github.com/evanw/esbuild/blob/v0.25.8/scripts/esbuild.js#L21 中身は下記 https://github.com/evanw/esbuild/blob/v0.25.8/lib/npm/node-install.ts