OpenVessel

ネイティブのモジュール

このページの見出し
  1. 概要
  2. プロセスから見えるもの
  3. 入れるときの決まり
  4. .node ファイルの読み込み
  5. node:wasi
  6. 動くときの napi-rs と emnapi
  7. 互換パッケージ
  8. 動かせないもの

ブラウザのタブは機械語を読み込めないので、OpenVessel はネイティブアドオン(.node ファイル)を決して動かしません。このページでは、その代わりに起きることを説明します。npm install がネイティブのパッケージを WebAssembly 版に差し替える決まり、.node ファイルを動かそうとしたときの失敗のしかた、node:wasi の実装、napi-rs の wasm32-wasi 版と emnapi の読み込み、OpenVessel が自分でビルドする互換パッケージです。

概要

やり方場所例
パッケージ自身の WebAssembly 版を、ネイティブのパッケージの名前で入れるインストーラー(src/kernel/npm/install.ts)esbuild → esbuild-wasm、rollup 4 → @rollup/wasm-node、lightningcss → lightningcss-wasm
napi-rs のパッケージの wasm32-wasi 版を、それに合う emnapi と一緒に入れるインストーラーrolldown・@node-rs/xxhash などの napi-rs のパッケージ
WebAssembly 版がある系列へ上げるインストーラーsharp < 0.33 → 0.34
OpenVessel がビルドした互換パッケージを入れるインストーラー+compat/・scripts/build-compat.mjsPagefind・cn-font-split・@resvg/resvg-js
WebAssembly を WASI preview1 で動かすnode:wasi(src/node/wasi.ts)上のうち WASI 向けにビルドされたものすべて
はっきり失敗させるモジュールの読み込み・process.dlopenそのほかの .node ファイル

パッケージマネージャの側(解決・置き場所・lockfile)はパッケージマネージャにあります。

プロセスから見えるもの

項目値意味
process.platform・process.archlinux・x64(両方の層)読み込み役はこれで環境ごとのパッケージを選びます。互換パッケージは、読み込み役が期待する場所では linux-x64 の名前で出しています
process.versions.napi10
process.report.getReport().header.glibcVersionRuntime2.36detect-libc がこれを読むので、musl でなく glibc の環境として数えられます
NAPI_RS_FORCE_WASIカーネルの既定の環境で 1(src/kernel/kernel.ts)napi-rs が生成する読み込み役が読みます。最近の読み込み役は true か error のときだけ WASI を強制し、ほかの値は「ネイティブを先に」と扱います。ネイティブ版は入っていない(環境での絞り込み)ので、どちらでも wasm32-wasi のパッケージに落ちます
インストーラーが使う環境os: linux・cpu: wasm32cpu が x64・arm64 などのパッケージは、入れるときに飛ばします

入れるときの決まり

決まりはすべて src/kernel/npm/install.ts にあります。どの入口(npm・pnpm・yarn・bun)にも、npx にも効きます。

順番

レジストリの依存では、resolve() が次の順に進めます。

  1. 指定から版を選ぶ(lockfile の手がかり・latest・合ういちばん高い版)。
  2. 系列の引き上げ: ネイティブだけの古い系列を、WebAssembly 版がある系列へ上げる。
  3. 互換パッケージ: その名前と版に登録があれば、それを使ってここで終わる。
  4. WebAssembly 版への置き換え: 中身を WebAssembly のパッケージに替える。

パッケージの依存を並べるときに、napi-rs と sharp の決まりが依存を足したり変えたりし、環境での絞り込みが動かせないパッケージを外します。

決して入れないもの

fsevents・bufferutil・utf-8-validate・cpu-features・ssh2 は、どこに書かれていても飛ばします(SKIP)。これらを require() すると MODULE_NOT_FOUND で失敗します。

WebAssembly 版への置き換え

SUBSTITUTES:

パッケージ中身の出どころ条件
esbuildesbuild-wasmどの版でも
rollup@rollup/wasm-nodeメジャー版が 4 以上
lightningcsslightningcss-wasmどの版でも
sass-embeddedsass(JavaScript だけの Dart Sass)どの版でも
  • 置き換え先の版は、同じ版があればそれ、なければ ~<version> のいちばん高い版、なければ ^<version> のいちばん高い版です。どれもないか、情報が取れなければ、元のパッケージをそのまま入れます。
  • パッケージは頼まれた名前と版のままです。node_modules/esbuild に置かれ、その package.json には "name": "esbuild"、解決した版、"_openvesselSource": "esbuild-wasm@<version>" が入ります。bin は置き換え先のもの、なければ元のものです。
  • 依存に使う manifest は置き換え先のものなので、ネイティブのパッケージの環境ごとの optionalDependencies (@esbuild/linux-x64 など)は要求しません。

系列の引き上げ

UPGRADES:

パッケージ版代わりに入れるもの
sharp<0.33.0^0.34.0 で決まる版

0.33 より前の sharp は libvips を .node アドオンとして同梱していました。0.33 以降には WebAssembly 版があり、呼ぶ側が使う範囲では API が同じです。package.json は変えません。入る版は範囲と食い違い、何も表示しません。

napi-rs の wasm32-wasi 版

napi-rs のパッケージは、環境ごとのバイナリのパッケージを optionalDependencies に並べます(@scope/pkg-linux-x64-gnu など)。出している場合は、@napi-rs/wasm-runtime と emnapi の上で動く WebAssembly 版(@scope/pkg-wasm32-wasi、cpu: ["wasm32"])もあります。

withWasiBinding(): すべてのパッケージ(プロジェクト自身も)の optionalDependencies で、wasm32 を含む名前がなく、-(linux|darwin|win32|freebsd|android)-(x64|arm64|arm|ia32) に -gnu・-musl・-msvc・-gnueabihf・-eabi が続いてもよい形で終わる名前があれば、最初のそのネイティブの項目と同じ指定で <base>-wasm32-wasi を足します。そのパッケージがなければ、optional の依存として npm warn optional skipping … を出して飛ばします。

withEmnapi(): 名前が -wasm32-wasi で終わり、dependencies に @emnapi/core がないパッケージには、 @emnapi/core@^1.4.0 と @emnapi/runtime@^1.4.0 を足します。emnapi 1.x でビルドした版は、@napi-rs/wasm-runtime の peer が見つける emnapi に頼ることが多く、それが別の版の emnapi 2 になることがあります(その実行時には、1.x のビルドが import する napi_set_last_error がありません)。emnapi 2 向けの版は自分で書いています。

pickWasmRuntime()(1.1.1 から): -wasm32-wasi のパッケージが @napi-rs/wasm-runtime に範囲で依存し、 @emnapi/core をそのままの版で固定しているとき、その範囲の中で、peerDependencies['@emnapi/core'] が固定された版を受け入れる(プレリリースも含めて)いちばん新しい @napi-rs/wasm-runtime を選びます。その peer を持たない版は受け入れます。合うものがない、版の emnapi がそのままの版でない、プロジェクトが @napi-rs/wasm-runtime を overrides している、のどれかなら、範囲をふつうに解決します。選んだ版はその版自身の node_modules に置くので、ほかのパッケージは拾いません。例: @emnapi/core@2.0.0-alpha.5 を固定し @napi-rs/wasm-runtime@~1.2.5 に依存する版は 1.2.5 になります。1.2.6 の peer は 2.0.0-beta.2 しか受け入れないからです。

DEP_PINS: WebAssembly 版の依存を、ビルドしたときの版に固定し、依存する側の隣に置きます。

パッケージ版依存固定する版
@img/sharp-wasm32<0.34.0@emnapi/runtime1.1.1

(これらのビルドの emscripten+emnapi 1.1 のつなぎは、@emnapi/runtime 1.2 で変わった callback-info のスタックを使います。)

sharp

決まり効果
UPGRADESsharp <0.33.0 → ^0.34.0
withSharpWasm()sharp >=0.35.0-0 で、optionalDependencies に @img/sharp-wasm32 がなければ @img/sharp-wasm32@<同じ版> を足す(0.35 は WebAssembly 版を @img/sharp-webcontainers-wasm32 として出すが、linux-x64 では require('@img/sharp-wasm32') に落ちる)
DEP_PINS@img/sharp-wasm32 <0.34.0 に @emnapi/runtime@1.1.1
環境での絞り込みネイティブの @img/sharp-linux-x64 と @img/sharp-libvips-* は cpu で外れる
// ふつうに `npm install sharp` した後で動く
const sharp = require('sharp');
const png = await sharp({ create: { width: 2, height: 2, channels: 3, background: 'red' } }).png().toBuffer();

環境での絞り込み

解決したパッケージは、os が linux を受け入れないか、cpu が wasm32 を受け入れなければ飛ばします(!win32 のような否定も読み、空のリストは何でも受け入れます)。optional でない依存なら npm warn EBADPLATFORM skipping <name>@<version> (needs …) を出します。libc と engines は調べません。互換パッケージは manifest を置き換えるので、os・cpu は互換パッケージのもの(なし)になります。

特別に扱う名前

インストーラーが特別に扱うパッケージの名前すべて:

名前決まり
fsevents・bufferutil・utf-8-validate・cpu-features・ssh2決して入れない
esbuildesbuild-wasm に置き換え
rollup(4 以上)@rollup/wasm-node に置き換え
lightningcsslightningcss-wasm に置き換え
sass-embeddedsass に置き換え
sharp<0.33 は引き上げ。>=0.35 には @img/sharp-wasm32 を足す
@img/sharp-wasm32<0.34 には @emnapi/runtime@1.1.1
@napi-rs/wasm-runtime版の emnapi に合わせて版を選ぶ(pickWasmRuntime)
@emnapi/core・@emnapi/runtime書いていない *-wasm32-wasi のパッケージに足す
*-wasm32-wasiemnapi を足す。napi-rs の環境ごとの optional の依存の隣に足す
@pagefind/linux-x64・cn-font-split・@resvg/resvg-js互換パッケージ(index.json から)

.node ファイルの読み込み

require() の解決は、.js・.json の次に .node も試します(両方の層)。そのため、ネイティブのファイルだけを持つパッケージは、ファイルを見つけたうえで読み込みに失敗します。

移植した層(src/nodejs/bindings/process.ts)旧来の層(src/node/module.ts・src/node/process.ts)
require('x.node')Node 自身の読み込みが process.dlopen を呼び、ERR_DLOPEN_FAILED を投げるERR_DLOPEN_FAILED を投げる: Native addons are not supported in OpenVessel (tried to load <file>). Use the WebAssembly build of this package.
process.dlopen()glibc の dlopen に似たメッセージ(下)で ERR_DLOPEN_FAILED を投げるコードのない Error を投げる: process.dlopen is not supported in OpenVessel (native addons cannot run in the browser)
import 'x.node'ERR_UNKNOWN_FILE_EXTENSION(--experimental-addon-modules なしの Node 22 と同じ)失敗する

移植した層の dlopen のメッセージ:

ファイルメッセージ
ない<file>: cannot open shared object file: No such file or directory
64 バイトより短い<file>: file too short
ELF の印で始まらない<file>: invalid ELF header
ELF のファイルNative addons (.node files) cannot run in OpenVessel. Use the WebAssembly build of the package.

napi-rs の読み込み役はこれらのエラーを受け止めて次の候補へ進み、最後に wasm32-wasi のパッケージにたどり着きます(動くときの napi-rs と emnapi)。

node:wasi

カーネルのファイルシステムの上の WASI preview1 で、src/node/wasi.ts にあります。両方の層が同じ import の関数を使い、その周りの JavaScript の API がちがいます。

API

移植した層旧来の層
モジュールinternalBinding('wasi')(src/nodejs/bindings/wasi.ts)の上の、Node 自身の lib/wasi.jssrc/node/wasi.ts の WASI クラスそのもの
version必須。'preview1' か 'unstable'。ない → ERR_INVALID_ARG_TYPE、ほかの値 → ERR_INVALID_ARG_VALUE無視する
getImportObject(){ wasi_snapshot_preview1: … }。'unstable' なら { wasi_unstable: … }いつも { wasi_snapshot_preview1: … }
設定の検査(args・env・preopens・stdin・stdout・stderr・returnOnExit)Node のものなし
env の値が undefined の項目捨てるKEY=undefined として残す
returnOnExit(既定 true)start() が終了コードを返す。false なら proc_exit でプロセスが終わる同じ
start(instance)_start が要り、_initialize は拒む(Node の検査)。2 度目は ERR_WASI_ALREADY_STARTED_start が要る(ERR_INVALID_ARG_TYPE)。2 度目は ERR_WASI_ALREADY_STARTED
initialize(instance)_start は拒み、_initialize があれば呼ぶ_initialize があれば呼ぶ
メモリinstance.exports.memoryinstance.exports.memory。または wasi.setMemory(memory) で渡す
ExperimentalWarning: WASI is an experimental feature…Node と同じく 1 回出る出ない
const { WASI } = require('node:wasi');
const fs = require('node:fs');

const wasi = new WASI({
  version: 'preview1',
  args: ['tool', ...process.argv.slice(2)],
  env: process.env,
  preopens: { '/': '/', '.': process.cwd() },
});
const instance = new WebAssembly.Instance(new WebAssembly.Module(fs.readFileSync('tool.wasm')), wasi.getImportObject());
process.exitCode = wasi.start(instance);

import の関数

preview1 の 46 の関数がすべてあります。ファイルに触る関数はカーネルのシステムコールを通るので、fs と同じメモリ上のファイルシステムが見えます。

関数動き
args_get・args_sizes_get・environ_get・environ_sizes_getargs と env から
clock_time_get時計 0(実時間): Date.now() をナノ秒にしたもの(ミリ秒の細かさ)。ほかの時計: performance.timeOrigin + performance.now()
clock_res_getいつも 1000 ns
fd_read・fd_write・fd_pread・fd_pwriteカーネルの read・write。iovec ごとに読み書きする
fd_seek・fd_tellカーネルの lseek
fd_closeカーネルの fd を閉じる(0〜2 は閉じない)
fd_fdstat_getファイルの種類・APPEND の印・すべての権利
fd_filestat_get・path_filestat_getカーネルの fstat・stat(path_filestat_get は SYMLINK_FOLLOW のときだけリンクをたどる)
fd_filestat_set_sizeカーネルの ftruncate
fd_filestat_set_times・path_filestat_set_timesカーネルの utimes
fd_readdir先頭に . と ..。d_next と d_ino は項目の番号+1
fd_renumber項目を移し、移し先は閉じる
fd_prestat_get・fd_prestat_dir_name前もって開いたディレクトリ。fd 3 から
path_openディレクトリはディレクトリの取っ手になる。CREAT・DIRECTORY・EXCL・TRUNC と fd の APPEND の印は open の flags に対応する。読み書きのどちらかは求めた権利から決める
path_create_directory・path_remove_directory・path_unlink_file・path_rename・path_link・path_symlink・path_readlink対応するカーネルの呼び出し
poll_oneoff時計の購読は、いちばん短い待ち時間(相対でも絶対でも)だけスレッドを止める(Atomics.wait)。fd の購読はすぐに準備できたと返し、呼び出しを止めない
random_getcrypto.getRandomValues を 64 KiB ずつ
proc_exitその終了コードで start() を終える(returnOnExit を参照)
sched_yield・fd_advise・fd_allocate・fd_datasync・fd_sync・fd_fdstat_set_flags・fd_fdstat_set_rights何もせずに成功する
proc_raise・sock_accept・sock_recv・sock_send・sock_shutdownENOSYS(52)

ファイルと preopens

  • fd 0・1・2 はプロセスの標準入力・標準出力・標準エラーです(stdin・stdout・stderr を渡せば、そのカーネルの fd)。
  • preopens の各項目 { ゲストのパス: ホストのパス } は、fd 3 からのディレクトリの fd になります。ホストのパスはプロセスの作業ディレクトリから解決します。
  • ディレクトリの fd からの相対パスは path.resolve(<ディレクトリ>, <パス>) で解決します。パスは前もって開いたディレクトリの中に閉じ込められず、.. で外へ出られます。
  • 権利は効かせません。どの fd もすべての権利を持つと答えます。

エラー

カーネルのエラーは WASI の errno の値で返します。

エラーerrnoエラーerrnoエラーerrno
E2BIG1EINTR27ENOTCONN53
EACCES2EINVAL28ENOTDIR54
EADDRINUSE3EIO29ENOTEMPTY55
EAGAIN6EISDIR31ENOTSUP58
EBADF8ELOOP32ENOTTY59
EBUSY10EMFILE33ENXIO60
ECANCELED11EMLINK34EPERM63
ECHILD12ENAMETOOLONG37EPIPE64
ECONNREFUSED14ENFILE41ERANGE68
ECONNRESET15ENODEV43EROFS69
EEXIST20ENOENT44ESPIPE70
EFAULT21ENOEXEC45ESRCH71
EFBIG22ENOMEM48ETIMEDOUT73
ENOSPC51EXDEV75
ENOSYS52ENOTCAPABLE76

そのほかのエラーは EIO(29)です。知らない fd は EBADF(8)、ファイルに DIRECTORY をつけた path_open は ENOTDIR(54)です。

スレッド

この WASI の実装には thread-spawn の import(wasi-threads)がありません。スレッドが要るモジュールは、自分の JavaScript でそれを用意します。napi-rs の wasm32-wasi 版は、WebAssembly.Memory を共有する worker_threads の Worker としてスレッドを起こします(OpenVessel のページは cross-origin isolated なので、共有メモリが使えます)。ふつうの wasm32-wasip1 向けにビルドした WebAssembly は 1 本のスレッドで動きます。Pagefind の互換パッケージは、そう動くようにパッチを当てています。

動くときの napi-rs と emnapi

napi-rs のパッケージが生成した読み込み役(index.js)は、まず環境ごとのネイティブ版を試します。どれも入っていないので、-wasm32-wasi のパッケージを読みます。そのパッケージの <name>.wasi.cjs は:

  • <name>.wasm32-wasi.wasm をコンパイルし、node:wasi の WASI を作り、@napi-rs/wasm-runtime と emnapi (@emnapi/core・@emnapi/runtime。WebAssembly の上に Node-API を実装するもの)を通してモジュールを動かします。
  • 版がスレッドを使うときは、wasi-worker.mjs から worker_threads の Worker を起こします。

入れるときの決まりがあるのは、この 3 つが合っていなければならないからです。版は自分をビルドした emnapi を固定し、 @napi-rs/wasm-runtime は使える emnapi を peer に書き、メジャー版のちがう emnapi は動かすときに(import が足りずに)失敗します。napi-rs の wasm32-wasi 版を参照してください。

互換パッケージ

互換パッケージは、ネイティブのコードしか持たない npm のパッケージの代わりです。OpenVessel がビルドしてランタイムと一緒に置き、インストーラーが元のパッケージの名前と版で入れます。互換パッケージの中身は semver の対象ではありません(CHANGELOG.md を参照)。

ビルドされるファイル

scripts/build-compat.mjs(scripts/build.mjs が呼ぶ。node scripts/build-compat.mjs で単独でも動く)が dist/runtime/compat/ に書きます。

ファイル中身
<id>-<version>.tgznpm の tarball にしたパッケージ(ustar。パスはすべて package/ の下)
index.json置き換えるパッケージの名前ごとに 1 項目
{
  "@resvg/resvg-js": {
    "range": "^2.0.0",
    "file": "resvg-js-2.6.2.tgz",
    "integrity": "sha512-…",
    "pkg": {
      "name": "@openvessel/resvg-js-wasm",
      "version": "2.6.2",
      "description": "@resvg/resvg-js on @resvg/resvg-wasm (OpenVessel)",
      "license": "MPL-2.0",
      "main": "index.js",
      "types": "index.d.ts",
      "dependencies": { "@resvg/resvg-wasm": "2.6.2" }
    }
  }
}
項目意味
キー置き換える npm のパッケージの名前
range代わりを務める、そのパッケージの版
filedist/runtime/compat/ の中の tarball の名前
integritytarball の sha512- の SRI のハッシュ
pkg互換パッケージの package.json。インストーラーはこの dependencies・bin などを使う

インストーラーでの使われ方

loadCompat() は <ランタイムの基点>/compat/index.json を、カーネルごとに 1 回取ってきます(ランタイムの基点は、カーネルのスクリプトが置かれたディレクトリです)。取れなかったり OK でなかったりすれば、そのセッションのあいだは空の一覧として扱います。

レジストリの依存で、版を選んで系列の引き上げをした後、パッケージの本当の名前(npm: の別名をたどった後)の項目が一覧にあり、版がその range に合えば(プレリリースも含む):

  • manifest は pkg に、頼まれたパッケージの name と version を入れたものです。
  • tarball は <ランタイムの基点>/compat/<file> です。ランタイムのオリジンからふつうの fetch で取り(関所は通りません)、integrity で確かめ、レジストリの tarball と同じようにキャッシュします。
  • 展開した package.json には、頼まれた name と version、"_openvesselSource": "<互換パッケージの名前>@<version>" が入ります。
  • WebAssembly 版への置き換えはしません。

配置先が compat/ を出していなければ、ネイティブのパッケージをいつもどおり入れようとします(そして環境での絞り込みで外れます)。

pagefind

置き換えるもの@pagefind/linux-x64 ^1.0.0(Pagefind の npm の包みが linux/x64 で選ぶバイナリのパッケージ)
パッケージ@openvessel/pagefind-wasi 1.5.2
ソースcompat/pagefind/
中身pagefind.wasm・bin/pagefind・bin/pagefind_extended

compat/pagefind/build.mjs は crates.io から pagefind 1.5.2 の crate を取り、compat/pagefind/wasi.patch を当てて cargo build --release --target wasm32-wasip1 --no-default-features(strip・LTO・codegen unit 1)を動かします。 cargo と rustup target add wasm32-wasip1 が要ります。結果は vendor/compat/pagefind/ にキャッシュします。パッチは target_os = "wasi" のときだけ、次のように変えます。

  • 複数スレッドの tokio の代わりに、1 本のスレッドの tokio(current_thread。features は rt・time・macros・ sync・io-util)を使う。
  • 出力を tokio::fs でなく std::fs で書く(create_dir_all と File だけの小さな wasi_fs モジュール)。
  • 索引づくりを spawn_blocking でなく、その場で動かす。
  • --service のモードの標準入力・標準出力の 2 本のスレッドを、1 つのループに替える。並んだ応答をすべて書き、次の要求(, で終わる base64 の JSON)を読むまで止まる。入力が終われば終了コード 0 で終わる。読めないメッセージにはエラーの応答を返す。

bin/pagefind と bin/pagefind_extended は同じ起動役です。args: ['pagefind', …argv]・env: process.env・ preopens: { '/': '/', '.': process.cwd() }・returnOnExit: true の node:wasi で動かし、process.exitCode にモジュールの終了コードを入れます。pagefind_extended は pagefind と同じビルドを動かすので、extended 版の CJK の単語分けはありません。pagefind パッケージのコマンド行と Node の API(createIndex() など。バイナリを --service のモードで動かすもの)は、ネイティブのバイナリと同じように動きます。

cn-font-split

置き換えるものcn-font-split ^7.0.0
パッケージ@openvessel/cn-font-split-wasi 7.4.3
ソースcompat/cn-font-split/
中身公開されている cn-font-split 7.4.3。dist/node/index.js と dist/node/index.mjs を差し替え、dist/libffi-wasm32-wasip1.wasm を足したもの

compat/cn-font-split/build.mjs は npm のレジストリから cn-font-split-7.4.3.tgz を、プロジェクトの GitHub のリリース 7.6.8 から libffi-wasm32-wasip1.wasm を取ってきます。manifest は公開されているものから、koffi の依存、 postinstall のスクリプト(ネイティブのライブラリをダウンロードするもの)、devDependencies を外したものです。

元の Node 向けの実装は、Koffi(FFI)でネイティブのライブラリを呼びます。差し替えた fontSplit(config) は、同じライブラリの wasm32-wasip1 版を node:wasi で動かします。

  1. config.input(パスかバイト列)を読み、config.subsets を変換し、設定をパッケージの protobuf の InputTemplate にします。outDir がなければ Error('cn-font-split need outDir')。
  2. 一時ディレクトリ(os.tmpdir()/cn-font-split-XXXXXX)を作り、fonts/<key>(設定)と空の <key>/ を置きます。 <key> はでたらめな値です。
  3. args: [key]・空の env・preopens: { '/tmp': <一時ディレクトリ> }・returnOnExit: true で、env の import モジュールに何もしない pthread_mutex_init・lock・unlock・destroy を渡して動かします。コンパイルしたモジュールは次の呼び出しのために残します。CN_FONT_SPLIT_BIN で .wasm ファイルのパスを変えられます。
  4. 終了コードが 0 でなければ Error('cn-font-split exited with <code>')。
  5. <key>/ の中のファイルをすべて、config.outputFile か fs-extra の outputFile で outDir に書きます。 config.silent でなければ名前を出します。かかった時間は console.time で出します。
  6. 一時ディレクトリを消します。

外に出すもの: fontSplit・createAPI・decodeReporter・proto(CommonJS と ESM)。1 本のスレッドで動きます。

resvg-js

置き換えるもの@resvg/resvg-js ^2.0.0(wasm32-wasi 版のない napi-rs のパッケージ)
パッケージ@openvessel/resvg-js-wasm 2.6.2。@resvg/resvg-wasm 2.6.2 に依存
ソースcompat/resvg-js/package/(index.js・index.d.ts)

resvg-js は同期で、フォントのファイルを自分で読みます。resvg-wasm は非同期で初期化し、フォントをバイト列で受け取ります。 index.js は:

  • resvg-wasm の CommonJS のつなぎに同期の初期化を足してコンパイルし、読み込んだ時点で index_bg.wasm を動かします。
  • new Resvg(svg, options): svg は文字列か Buffer。options.font.fontFiles と options.font.fontDirs (.ttf・.otf・.ttc・.otc・.woff・.woff2 を下の階層まで探す)を読み、渡された fontBuffers と合わせて fontBuffers にします。loadSystemFonts は無視します(システムのフォントはありません)。ほかの設定はそのまま resvg-wasm に渡します。
  • renderAsync(svg, options, signal) は new Resvg(svg, options).render() の Promise を返します。signal がすでに中止されていれば拒否します。

外に出すもの: Resvg・renderAsync。index.d.ts に resvg-js の型があります。

const { Resvg } = require('@resvg/resvg-js');
const png = new Resvg('<svg xmlns="http://www.w3.org/2000/svg" width="4" height="4"/>', {
  font: { fontDirs: ['./fonts'] },
}).render().asPng();

互換パッケージの足し方

  1. パッケージ自身のファイルを compat/<id>/package/ に置きます。土台(あれば)の上にコピーされ、package.json は項目の pkg から書かれます。
  2. ビルドやダウンロードが要るなら、outDir(vendor/compat/<id>/。git の外)に書く async function build(outDir) を出す compat/<id>/build.mjs を足します。項目に書いた成果物が欠けているときだけ動きます。例外を投げると、ビルドは compat <id>: not built (<message>); <names> will stay unavailable を出し、その項目を飛ばします。
  3. scripts/build-compat.mjs の COMPAT に項目を足します。
項目意味
idcompat/ と vendor/compat/ の下のディレクトリの名前。tarball の名前の頭にもなる
names置き換える npm のパッケージの名前(それぞれ index.json の項目になる)
range置き換える、それらのパッケージの版
pkgpackage.json のオブジェクト。または土台の package.json を受け取って返す関数。その version が tarball の名前になる
artifacts省略可。パッケージにコピーする { <vendor/compat/<id> の中のパス>: <パッケージの中のパス> }
base省略可。始めにする vendor/compat/<id>/ の中のディレクトリ(展開した元のパッケージなど)
  1. node scripts/build-compat.mjs を動かし、dist/runtime/compat/index.json にパッケージが載ったか確かめます。 OpenVessel で元のパッケージを入れ、その package.json の _openvesselSource を確かめます。
{
  id: 'example',
  names: ['example-native'],
  range: '^2.0.0',
  pkg: { name: '@openvessel/example-wasi', version: '2.1.0', main: 'index.js', license: 'MIT' },
  artifacts: { 'example.wasm': 'example.wasm' },
}

互換パッケージは元の API を保ちます。読み込み役や呼ぶ側は、元の名前のまま require します。包みのパッケージが動かすバイナリなら(Pagefind のように)、包みが linux・x64 で選ぶ環境ごとのパッケージの名前で互換パッケージを作ります。

動かせないもの

WebAssembly 版も互換パッケージもないネイティブのコードは動きません。.node アドオン全般、Koffi のような FFI のライブラリ、workerd のように自前のエンジンを抱えたプログラム、ヘッドレスのブラウザです。依存の install スクリプトは動かさないので、postinstall でバイナリをダウンロードしたりコンパイルしたりするパッケージはそれを得られません。理由つきの一覧は互換性にあります。

この文書の元は、リポジトリの docs/reference/native-modules.ja.md です(英語版は native-modules.md)。