アーキテクチャ
OpenVessel は、自分の OS(ファイルシステム・プロセス・シェルを持つカーネル)と、自分のプログラム(Node.js・npm・Astro)と、決まった出入り口(外への通信のネットワークゲートと、プレビューのための仮想ポート)を持っています。仮想マシンと違うのは、CPU を再現しないことです。JavaScript と WebAssembly は、ブラウザのエンジンでそのまま動きます。
システム構成
ブラウザのタブの中に、3つのオリジンを置きます。埋め込む側のページ(H)、ランタイム(R)、プレビュー(P)です。3つは postMessage だけで話します。
H
埋め込む側のアプリと API。仮想マシンの中のコードは、このページの DOM・クッキー・ストレージに触れられない。
R
カーネル・プロセス・ネットワークゲート。外に出られるのはここだけで、それもネットワークゲートを通るときだけ。
P
開発中のサイト。サイト自身の JavaScript が、ランタイムや H に届かないように分ける。
レイヤー構成(Node.js との対比)
Node.js は、JavaScript で書かれた lib/ が、C++ の internalBinding()・libuv のイベントループ・OS の上に乗っています。OpenVessel は lib/ をそのまま使い、その下の層だけをブラウザの上に作り直しました。
本物の Node.jsLinux / macOS / Windows
OpenVesselブラウザのタブ
色のついた層が、OpenVessel が作った部分です。上の2段は同じものなので、エラーの文言とコード、引数の検査、node:fs:441:20 のようなスタックトレース、ストリームの背圧、イベントの順番が Node のとおりになります。
syscall
同期システムコール
プロセスは要求を SharedArrayBuffer に書き、カーネルが答えるまで Atomics.wait で眠ります。本物の OS と同じ形なので、fs.readFileSync・spawnSync・同期の require() がブラウザで動きます。
ES モジュール
ネイティブ ES モジュール
ソースをランタイムの Service Worker から配信するので、import・import.meta・最上位の await は、ブラウザのモジュールそのものとして動きます。
WebAssembly
ネイティブアドオンの WebAssembly 化
esbuild・rollup・rolldown・lightningcss・sharp・napi-rs の wasm32-wasi 版を node:wasi で動かします。無いものは互換パッケージ(Pagefind・cn-font-split・@resvg/resvg-js など)で補います。
カーネルの機能
Web Worker 1つが OS の役をします。node のプロセスは、それぞれが1つの Web Worker です。
- メモリ上のファイルシステム(権限のビット・時刻・監視)
- プロセス(pid・ファイル記述子・環境変数・終了コード・シグナル)
- パイプ・端末(TTY)・ソケット(TCP / Unix の socketpair)
- sh と coreutils(ls・cat・grep・ps など)
- npm・pnpm・yarn・bun(4つの lockfile を1つのクライアントで)
- 仮想ポートとネットワークゲート
処理の流れ:npm install && npm run dev
-
01コマンドを渡す
vessel.exec()埋め込む側のページが API を呼ぶと、カーネルが sh を起動し、sh がコマンドを読み解きます。
-
02依存を入れる
npm installnpm と同じように依存の木を解き(持ち上げ・peer 依存・lockfile)、ネットワークゲートを通して tarball を取り、sha512 を確かめてからメモリ上のファイルシステムに展開します。ネイティブのパッケージは WebAssembly 版か互換パッケージに差し替えます。
-
03Node.js を起動する
npm run dev新しい Web Worker で Node.js が立ち上がります(温めておいた Worker なら約 0.05 秒)。Astro と Vite を ES モジュールとして読み込み、esbuild と rolldown を WebAssembly で動かします。
-
04仮想ポートで待ち受ける
listen(4321)カーネルが待ち受けを記録し、埋め込む側のページに server-ready を知らせます。ページがプレビューの iframe を作ります。
-
05編集が反映される
fs.writeFile()ファイルを書くと、カーネルが監視している側に知らせます。Vite の fs.watch が反応し、HMR の知らせが WebSocket でプレビューに届きます。
Node.js のバージョン追従
参照している Node の版はソースの1か所(src/node/version.ts)にあり、新しい版との差分はスクリプトで洗い出せます。lib/ は Node のものをそのまま使うので、追従は「変わった internalBinding を読み直す」作業になります。似せて作った互換の層のように、振る舞いを一つずつ追いかけ直す必要がありません。
バインディングは1つずつ、本物の Node で同じ呼び出しをした結果と突き合わせて試験しています。Node 自身の試験も OpenVessel の中で流しています。
| src/kernel/ | カーネル:VFS・プロセス・I/O・syscall・ネットワーク・sh・coreutils・npm |
|---|---|
| src/nodejs/ | Worker の中の Node.js:起動・イベントループ・internalBinding(1つずつ1ファイル) |
| src/runtime/ | ランタイムの iframe と Service Worker(ES モジュールの配信・ネットワークゲート) |
| src/preview/ | プレビューの中継と Service Worker(HTTP・WebSocket を仮想ポートへ) |
| src/host/ | 埋め込む側のページのための API |
| compat/ | ネイティブだけのパッケージの互換パッケージ(WebAssembly にコンパイル) |