Share
Embed
Node.js の exe 化と U16 プロコン CHaser
hokkaido.js vol.08@Asahikawa w/ ゆるい勉強会@旭川 にて発表しました。
https://hokkaido-js.connpass.com/event/407987/
Ryota Kunisada
October 31, 2026
Transcript
22 slides
-
Node.js の exe 化と U16 プロコン CHaser hokkaido.js vol.08@Asahikawa w/
ゆるい勉強会@旭川 国定 凌太 @92thunder 1 / 22 -
国定 凌太 @92thunder テックタッチ株式会社のプロダクトオーナー 全ての Web システムの価値を向上するサービスを開発 岡山県 津山高専 →
ソニーデジタルネットワークアプリケーションズ → テックタッチ株式会社(当時創業1ヶ月 1人目社員) 2023年に北海道に移住 U16プロコンを題材に小中学生向けプログラミング教室をしてい ます 2 / 22 -
今年も東神楽から2人参加します U-16 プログラミングコンテスト 旭川大会の競技部門(CHaser) 今年は夏休みの講習会を開けなか った… 去年参加してくれた子に声をかけ たら 去年と同じ2人が、今年も出て くれることに!
CHaser の対戦画面(github.com/u16procon/chaser-server) 3 / 22 -
去年工夫していたこと 環境構築は代わりにやる TypeScript を使う node main.ts で直接実行 Biome で保存時にフォーマット コピペなどのショートカットを覚
えてもらう AI の活用をどうする? 5 / 22 -
今年はプログラムの exe 化に取り組む 大会は Python が前提の環境 Node.js のソースコードのままでは提出できない 出るには自分の PC
を持ち込んで、朝の接続動作確認などが必要 exe 1ファイルにすれば PC を持っていかなくていい! 6 / 22 -
そもそも exe って何? Windows でダブルクリックすると動くプログラムのファイル(実行ファイル) JavaScript はそのままでは動かない → Node.js(ランタイム)が読んで実行している いつも:2つそろって動く
node.exe Node.js ランタイム (インストールが必要) SEA で exe にすると:1ファイルで動く + main.ts 自分のプログラム main.exe Node.js ランタイム 自分の プログラム node main.ts main.exe をダブルクリック 7 / 22 -
Node.js のプログラムを exe にする選択肢 方法 提供元 状態 TypeScript ひとこと Node.js
SEA node --build-sea Node.js Experimental 25.5 から1コマンド esbuild で バンドル 純正。Node.js のまま 基本は JS 1ファイル pkg Vercel 2024年にアーカイブ 事前にビルド 昔の定番。もう更新されない @yao-pkg/pkg コミュニテ ィ 開発が続いている 事前にビルド pkg の後継。SEA モードも ある nexe コミュニテ ィ 5.0 が beta のまま 2025年3月から更新なし 事前にビルド Node.js をビルドすること も bun build --compile Bun 安定 2025年12月 Anthropic が 買収 そのまま OK 中身は Bun ランタイム deno compile Deno 2026年10月 Cloudflare へ 開発は約1年で終了予定 そのまま OK 中身は Deno ランタイム どれも exe はだいたい 100MB 前後(ランタイムごと入るため)。2026年10月時点で調べた内容です。 8 / 22 -
今回選んだのは Node.js SEA Single Executable Applications:Node.js 本体とプログラムを1つの実行フ ァイルにまとめる純正の仕組み Bun や
Deno に乗り換えず、教室で使っている Node.js のままでいける v25.5 で node --build-sea が入り、1コマンドで作れるようになった それまでは blob を作って postject で node.exe のコピーに埋め込む…と手順が 多かった まだ Experimental(Stability 1.1 Active development) 起動のたびに出る警告は disableExperimentalSEAWarning で消せる "mainFormat": "module" で ES モジュールのまま動く。トップレベル await も 動いた 9 / 22 -
SEA の仕組み:node.exe に「荷物」を埋め込む exe を作るとき( node --build-sea の中でやっていること) main.js 5,176
バイト → ① blob に する main.exe blob ヘッダー 33B main.js そのまま → ③ 埋め込む 5,209 バイト ② node.exe のコピー(約 100MB) Node.js ランタイム NODE_SEA_BLOB (blob) ④ fuse …:0 → …:1 exe を起動したとき main.exe を ダブルクリック → node.exe として起動 → blob が 入ってる? ある → blob の main.js を実行 ない → 普通の node.exe 10 / 22 -
blob の中身を見てみた(chaser-nodejs の main.js) blob だけ書き出す(昔の手順の1つ目) sea-prep.blob の先頭(xxd) node --experimental-sea-config
sea-config.json main.js sea-prep.blob 5,176 バイト 5,209 バイト(33 バイト増えただけ) 20da 4301 0100 0000 0101 0700 0000 0000 0000 6d61 696e 2e6a 7338 1400 0000 0000 002f 2f20 6368 6173 6572 2e74 730a 696d .. main.js 8 ...... // chaser.ts.im 先頭から順に読むと ヘッダー 33 バイト 20 da 43 01 01 00 00 00 01 01 07 00 00 00 00 00 00 00 main.js 38 14 00 00 00 00 00 00 // chaser.ts … ↑ ↑ ↑ ↑ ↑ ↑ ↑ 設定 ファイル名の長さ =7 ファイル名 中身の長さ 0x1438 = 5,176 main.js の 中身そのまま 目印 フラグ (SEA の blob) (警告を消す) 長さは 8 バイトで、小さい桁から並ぶ(リトルエンディアン)。5,176 は main.js のサイズとぴったり同じ ※ blob の中の形式は公式ドキュメントには書かれていない(バージョンで変わるかも) 11 / 22 -
埋め込んだ exe の中を見てみた node.exe と main.exe の大きさを比べる node.exe main.exe 105,319,752
バイト 105,467,208 バイト exe をテキストとして NODE_SEA_FUSE_ で検索すると( grep -ao ) > grep -ao 'NODE_SEA_FUSE_[0-9a-f]*:[01]' node.exe main.exe node.exe:NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0 main.exe:NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2:1 fuse は「blob を探しに行くか」を決めるスイッチ node.exe に最初から入っている決まった文字列で、埋め込むと最後の 0 が 1 に書き換わる 起動したときに 1 なら、埋め込まれた blob を読んで実行する 同じように検索すると、main.exe の中に // chaser.ts がそのまま見つかる 12 / 22 -
chaser-nodejs で exe ができるまで main.ts chaser.ts TypeScript 2ファ イル →
esbuild でバンド ル main.js JS 1ファイル (5KB) → node --buildsea node.exe のコ ピー + main.js → 実行中の Node.js を 使う main.exe 約 100MB 実行できるのは JS 1ファイルだけ → ここだけ外部ツール(esbuild)が必要 組み込みモジュール( node:net など)以外は import できないのでバンドルする 埋め込む先は、ビルドした Node.js と同じバージョンの node.exe だけ 13 / 22 -
Node.js 25.5 で 1コマンドになった 〜25.4:手順を全部自分でやる(postject は npm の外部ツール) # 1.
blob(sea-prep.blob)を作る node --experimental-sea-config sea-config.json # 2. node.exe をコピーする node -e "require('fs').copyFileSync(process.execPath, 'main.exe')" # 3. blob を埋め込んで fuse をオンにする npx postject main.exe NODE_SEA_BLOB sea-prep.blob ` --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 25.5〜: --build-sea がまとめてやってくれる node --build-sea sea-config.json Node.js 26(10月に LTS)にも入っている。Node.js 24 にはない 14 / 22 -
chaser-nodejs での設定 sea-config.json { "main": "build/main.js", "mainFormat": "module", "output": "dist/main.exe",
package.json の scripts.build(読みやすく改行) esbuild main.ts --bundle --platform=node --format=esm --outfile=build/main.js && node --build-sea sea-config.json "disableExperimentalSEAWarning": true } 生徒は npm run build するだけ npm のパッケージは使わず、Node.js に最初から入っている node:net と node:readline だけで動く ダブルクリックで起動 → CHaser の接続 IP やポートを確認 15 / 22 -
やってみて気づいたこと・まだわからないこと exe は約 100MB(中身はほぼ node.exe) 署名なしなので SmartScreen に止められるかも(未確認) 入力が途中で終わると、何も言わずに終了する node
main.ts なら警告が出るのに… ダブルクリック起動だと、終わるとすぐ閉じるかも(未確認) 16 / 22 -
デモ ① exe をビルドする > npm run build build\main.js ビルドにかかる時間(手元で3回測定)
esbuild(バンドル) 10〜70ms --build-sea(exe 作成) 500〜700ms npm run build 全体 約 1,700ms 5.1kb Done in 35ms Generated single executable C:\Program Files\nodejs\node.exe + sea-config.json -> dist/main.exe 初回は約 3,000ms。全体は npm の起動込み 結論:本物の CHaser サーバーにつない で動いた! ② exe を起動して、本物のサーバーにつなぐ Node.js が入っていなくても動く dist\main.exe 17 / 22 -
おまけ:コードキャッシュとスナップショット どちらも起動を速くするために exe の中に入るデータ(ビルド用のキャッシュではない) コードキャッシュ( useCodeCache ) V8 によるコンパイルを事前に済ませて送る スナップショット(
useSnapshot ) 準備が終わった状態をセーブして送る ビルド時に main.js をコンパイルした結果を ビルド時に main.js を動かし、入力を待つ直前ま exe に入れておく 起動時はゼロからコンパイルしなくていい で準備する そのときのメモリを保存して exe に入れる 起動時はセーブデータから続きを始める V8 の内部形式なので、作った OS・CPU でしか読めない chaser-nodejs ではどちらも使っていない(初期値は false ) スナップショットは "mainFormat": "module" とは一緒に使えない 20 / 22 -
おまけ:別の OS 向けの実行ファイルも作れる sea-config.json の "executable" に、作 りたい OS 用の
node を指定する sea-config.json(Linux x64 用の例) { "main": "build/main.js", "executable": "node-linux-x64/bin/node", 省略すると、いま動いている node が 使われる "output": "dist/main-linux", "useCodeCache": false, node のバージョンは同じにする useCodeCache と useSnapshot は false "useSnapshot": false } コードキャッシュとスナップショットは、 作った OS でしか読めない node --build-sea sea-config.json Mac 用は codesign --sign - で署名が必 要。Windows 用は署名なしでも動く 出典:Node.js 公式ドキュメント nodejs.org/api/single-executable-applications.html 21 / 22 -
おまけ:VFS(仮想ファイルシステム) どんなときに使う? HTML・画像・設定ファイルなど、JS 以外のファイルを読むアプリや、バンドルしにくい既存のアプリを exe にしたいとき exe に入れたファイルを、いつもの fs で読める(v26.9.0
から。まだ Early development) VFS なし:SEA 専用の API で読む const { getAsset } = require('node:sea'); const config = getAsset('config.json', 'utf8'); 中のファイルが読み取り専用のフォルダに見える 相対パスの require / import や node_modules もその中から読める ZIP ごと埋め込める( vfsArchive )。exe が小さくな る VFS あり(assets + "useVfs": true ):いつもの fs const config = fs.readFileSync( path.join(__dirname, 'config.json'), 'utf8'); 書き込むとエラー( EROFS ) コードキャッシュ・スナップショットとは一緒に使えない .node は一時ファイルに書き出して読む chaser-nodejs はほかのファイルを読まないので不 要 出典:Node.js 公式ドキュメント nodejs.org/api/single-executable-applications.html 22 / 22