🎩

CF WorkersでNodeJS依存ライブラリが使えない?できらぁ!!!~黒魔術ViteプラグインでNodeJS依存関数を透過的に呼ぶ話~

に公開

この記事は、Cloudflare Advent Calendar 2025の二十日目の記事です。

はじめに

みなさん、Cloudflare Workers使ってますか!? 安くて、他のCloudflare製品群とService Bindingsで簡単に結合出来て、課金体系がLLMと相性が良くて最高なので、僕はよく使っています。

これらのメリットは、Cloudflare Workers が V8 ベースの独自ランタイムで動くことに起因しますが、同時に大きなデメリットももたらしてしまいます。つまり、@napi-rs/canvas、pdfjs-dist、playwright など NodeJS への依存が強いライブラリを利用できない、という点です。

これまで僕は、NodeJS 依存部分を Stateless なコンテナとして Cloud Run などに切り出し、HonoRPC 経由で Workers から呼び出すという、大変冗長な事をしていました。(まぁ Workers が BFF/プロキシ用途を主に想定していることを踏まえると、この構成自体は筋が悪いわけではないと思いますが...)

このCF Workersユーザーが長年悩んできた問題にピリオドを打つべく、超黒魔術的なViteプラグインを作ったので、紹介したいと思います。

できたもの

https://github.com/inaridiy/create-nodejs-fn

というわけで、create-nodejs-fnというViteプラグインを作って、公開しました。まず初めに、当Viteプラグインを導入した後にCF Workersで実行できる、この世の終わりみたいなコードをお見せしましょう。

src/clock.container.ts
import { createCanvas } from "@napi-rs/canvas";
import { nodejsFn } from "./__generated__/create-nodejs-fn.runtime";

export const renderClock = nodejsFn(async () => {
  const canvas = createCanvas(600, 200);
  const ctx = canvas.getContext("2d");

  ctx.fillStyle = "#1a1a2e";
  ctx.fillRect(0, 0, 600, 200);

  ctx.font = "bold 36px 'Noto Sans CJK JP', 'Noto Color Emoji', sans-serif";
  ctx.fillStyle = "#eee";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";

  const now = new Date().toISOString();
  ctx.fillText(`🕐 ${now}`, 300, 100);

  return await canvas.toDataURLAsync("image/webp");
});
src/index.ts
import { Hono } from "hono";
import { renderClock } from "./clock.container";

const app = new Hono();

app.get("/clock", async (c) => {
  const webpDataUrl = await renderClock();
  return fetch(webpDataUrl);
});

export { NodejsFnContainer } from "./__generated__/create-nodejs-fn.do";
export default { fetch: app.fetch };

はい。@napi-rs/canvasという、どう見てもCF Workersで動かないライブラリが、平然とsrc/index.tsから直接importされ、平然と呼び出されています。特別目に付くのは、nodejsFnという謎の関数で@napi-rs/canvas呼び出し部分がラップされているぐらいでしょうか?

そして、このコードは動きます。このコードをnpm run devで起動し、http://localhost:5173/clockにアクセスすると...

ちゃんとCanvasでレンダリングされた画像が表示されます。しれっと絵文字も使えてますね。

前述のリポジトリのQuick Startとまったく同じコードなので、皆さんの手元でも動かしてみてください(記事では省いていますが、パッケージのインストールや軽いセットアップが必要です)。

はい。こんな奇妙なことがなぜできるのでしょうか?

黒魔術のからくり

この黒魔術は、Cloudflare Containersのリリースと、@cloudflare/vite-pluginの成熟に支えられています。

Cloudflare Containers とは

Cloudflare Containersとは、Cloudflareが最近公開したサーバーレスコンテナプラットフォームで、Cloudflare Workersから呼び出せるDockerコンテナを簡単にデプロイできます。

Dockerコンテナが動くということは、何でも動かせるわけです。もちろんNodeJSも。ので、当プラグインはこれをフル活用します。

余談ですが、CF ContainersはGCPのCloudRunと色々異なり、プログラマブルにStatefullなふるまいを制御できたりします。これをうまく使った製品が、Cloudflare Sandboxです。

@cloudflare/vite-plugin とは

Cloudflare Workers を開発する場合、以前は wrangler CLI で開発環境の立ち上げ、ビルド、デプロイを行うのが一般的で、Vite でバンドルすることはできませんでした。結果として Vite プラグインが使えず、フロントエンドも絡めた開発などでは、面倒くさい事態が多発していました。

そんな中で登場したのが @cloudflare/vite-plugin です。こいつ使えば、Hot Reload や Service Binding など、wrangler dev 相当の開発体験を維持したまま Vite プラグインをぶっ刺せます(import {env} from "cloudflare:workers"出来るのようになったのも効いてますね)。さらに Vite Environment API の上で動くので、フロントエンドともいい感じに統合して実装できます。

とにかく、CF Workersの開発でもViteプラグインを差し込めるようになったわけです。

create-nodejs-fnが何をするか

create-nodejs-fnは、前述の二つの技術を基に、次の事をするViteプラグインです。

  1. nodejsFn部分の検出と解析: *.container.ts にマッチするファイルをts-morphで解析し、nodejsFnで囲われている関数を抽出します。
  2. Cap'n WebのHTTPサーバー生成: 抽出した関数群を、capnwebというRPCライブラリを使ったRPC on HTTPサーバーとしてラップし、esbuildでバンドルします。
  3. Dockerfile自動生成: プラグインのオプション(baseImage、systemPackages、preInstallCommandsなど)を基に、コンテナをビルドするためのDockerfileを自動生成します。@napi-rs/canvasのようなネイティブ依存がある場合でも、systemPackagesに必要なパッケージを指定するだけでOKです。
  4. プロキシコード生: Workers側でnodejsFnを呼び出している部分を、CF Containers経由でRPCリクエストを発行するプロキシコードに差し替えます。ViteのresolveIdフックで*.container.tsのimportを横取りし、生成済みのプロキシファイルに解決することで実現しています。

最高にロックでマッドですね(自画自賛)。要するに、nodejsFnで囲っておけば、その中身は自動的にコンテナ化され、Workers側からは透過的に呼び出せる開発体験になります。

ちなみに、そもそもCF WorkersからNodeJS依存なコードを呼ぶなら、外部のNodeJSランタイムに外出しして、HTTPで呼ぶことになるので、このプラグインのオーバーヘッドはそれと同等といえます。むしろ、手動でHTTPクライアントを書いたり、型定義を二重管理したりする手間が省けるので、トータルでは開発効率が上がるはずです。

このプラグインのこだわり

このプラグインは大半Vibe Codingで作ったわけですが、以下のような機能を備えており、出来るだけスムーズに開発できるようになっています。

1. initコマンドによる自動セットアップ

pnpm exec create-nodejs-fn initを実行すると、既存のWorkersプロジェクトに必要な設定を自動で追加してくれます。具体的には

  • wrangler.jsoncへのcontainers設定の追加
  • Durable Objectのバインディング設定
  • src/__generated__/ディレクトリの作成
  • エントリファイル(index.tsなど)へのNodejsFnContainerのexport追加
  • .gitignoreへの生成ファイルの追加

これで、vite.config.tsに多少設定を追記すると、すぐこのプラグインが使える状態になります!!

2. Hot Reloadによるコンテナ自動リビルド

npm run dev中に*.container.tsファイルを編集すると、プラグインが変更を検知して自動でコンテナをリビルドし、Vite開発サーバーを再起動してくれます。debounce処理も入っているので、連続した変更でも無駄なリビルドが走らないようになっています。

3. 型安全なRPC呼び出し

生成されるプロキシコードは、元のnodejsFnの型情報を保持しているので、Workers側での呼び出しは完全に型安全です。TypeScriptの恩恵を受けながら、まるでローカル関数を呼ぶかのような体験で開発できます。

4. containerKeyによるインスタンス制御

nodejsFnの第2引数でcontainerKeyを指定すると、引数に応じて異なるコンテナインスタンスにルーティングできます。これで、負荷分散したり、頑張ったらStatefulなこともできるはずです。

おわりに

ここまで読んでくださりありがとうございます。もうお分かりかと思いますが、このプラグインは非常に危険でバグったらヤバいので、本番環境では使わないでください。

それでも、かなり面白いものだと思うので、ぜひ手元で動かしてみてください!!!きっと変な笑いが出ると思います。面白い使い方が出来たら、Twitterなどでツイートしてもらえると嬉しいです

Discussion