📌

Cloudflare Workersのnodejs_compat_v2で何が変わったのか

2024/10/20に公開

nodejs_compat_v2 とは

nodejs_compat_v2 は、Cloudflare Workers ランタイムで Node.js API を有効にする互換性フラグです。 wrangler.toml ファイルに追加することで、組み込みの Node.js API とポリフィルを使用できるようになり、Node.js モジュールを "node:" 接頭辞なしでインポートし、nodejs_compat では利用できないポリフィルされた Node.js モジュールとグローバルを使用できるようになります。

従来の nodejs_compat フラグと比較して、nodejs_compat_v2 は、より多くの Node.js API とモジュールへのアクセスを提供します。 compatibility_dateが 2024-09-23 以降に設定されている限り、nodejs_compat 互換性フラグは nodejs_compat_v2 互換性フラグとまったく同じ動作をするようになります。

nodejs_compat_v2 を有効にすると、Wrangler は unenv を使用して Node.js API の使用を自動的に検出し、必要に応じてポリフィルを追加します。 これにより、サーバーレス関数のコンテキストでは動作しない Node.js API も解決しつつ、既存の npm パッケージとの互換性を高めることができます。

以下の各ランタイムのNode.js互換性カタログサイトで、nodejs_compat_v2 を有効にした場合に使用できる Node.js API の一覧を確認できます。workerdという項目がCloudflare Workersのランタイムがネイティブにサポートしているかで、wranglerの項目がWranglerがデプロイ時にポリフィルで吸収できるかです。

https://workers-nodejs-compat-matrix.pages.dev/

ネイティブなEventEmitterを使う

eventsがネイティブでサポートされたということは各モジュールで広く使われているEventEmitterもサポートされるということです。これを使ってみます。

npm create cloudflare@latest -- my-worker
cd my-worker
wrangler.toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-10-18"
compatibility_flags = ["nodejs_compat"] # nodejs_compat_v2として認識される
src/index.ts
import { EventEmitter } from 'events';

export default {
	async fetch(request, env, ctx): Promise<Response> {
		const emitter = new EventEmitter();
		emitter.on('hello', (...args: any[]) => {
			console.log(...args);
		});

		emitter.emit('hello', 1, 2, 3);
		return new Response('Hello World!');
	},
} satisfies ExportedHandler<Env>;

npm run dev
curl http://localhost:8787/

[wrangler:inf] Ready on http://localhost:8787
1 2 3
[wrangler:inf] GET / 200 OK (9ms)

csv-stringifyを使ってみる

次にネイティブなstream.Transformが動作することを確認するために、csv-stringifyを使ってみます。

npm install csv-stringify
src/index.ts
import { stringify } from 'csv-stringify';

export default {
	async fetch(request, env, ctx): Promise<Response> {
		const data = [
			{ name: '田中太郎', age: 30, city: '東京' },
			{ name: '佐藤花子', age: 25, city: '大阪' },
			{ name: '鈴木一郎', age: 40, city: '名古屋' },
		];

		const csvData = await new Promise<string>((resolve, reject) => {
			stringify(data, { header: true }, (err, output) => {
				if (err) reject(err);
				else resolve(output);
			});
		});

		return new Response(csvData, {
			headers: { 'Content-Type': 'text/csv' },
		});
	},
} satisfies ExportedHandler<Env>;
curl http://localhost:8787/
name,age,city
田中太郎,30,東京
佐藤花子,25,大阪
鈴木一郎,40,名古屋

osモジュールのポリフィルを確認する

osモジュールはネイティブでサポートされていないので、nodejs_compat_v2 を有効にしてもポリフィルされます。それがどのように動作するかを確認します。

src/index.ts
import os from 'os';

export default {
	async fetch(request, env, ctx): Promise<Response> {
		const systemInfo = [
			`ホスト名: ${os.hostname()}`,
			`プラットフォーム: ${os.platform()}`,
			`CPU アーキテクチャ: ${os.arch()}`,
			`CPU コア数: ${os.cpus().length}`,
			`空きメモリ: ${(os.freemem() / (1024 * 1024)).toFixed(2)} MB`,
			`合計メモリ: ${(os.totalmem() / (1024 * 1024)).toFixed(2)} MB`
		].join('\n');

		return new Response(systemInfo, {
			headers: { 'Content-Type': 'text/plain' },
		});
	},
} satisfies ExportedHandler<Env>;
curl http://localhost:8787/
ホスト名: 
プラットフォーム: linux
CPU アーキテクチャ: 
CPU コア数: 8
空きメモリ: 0.00 MB
合計メモリ: 0.00 MB

workerをCloudflare側でも動作させてみます。

wrangler dev -r

curl http://localhost:8787/
ホスト名: 
プラットフォーム: linux
CPU アーキテクチャ: 
CPU コア数: 8
空きメモリ: 0.00 MB
合計メモリ: 0.00 MB

実際にデプロイもしてみます。

wrangler deploy

❯ curl  https://my-worker.laiso.workers.dev
ホスト名:
プラットフォーム: linux
CPU アーキテクチャ:
CPU コア数: 8
空きメモリ: 0.00 MB
合計メモリ: 0.00 MB

同じ結果を返すのでunenvのモックがあることがわかります。定義は以下から確認できます。

https://github.com/unjs/unenv/blob/f9786126f0da264725113eb544fc938a55f078c4/src/runtime/node/os/index.ts

バンドルされたJSにもunenvのポリフィルが埋め込まれているのを確認できました。

wrangler deploy src/index.ts --dry-run --outdir out
grep "__unenv__" out/index.js
  return Object.assign(fn2, { __unenv__: true });
}, { __unenv__: true });
      if (prop === "__unenv__") {
  __unenv__ = true;
  __unenv__ = true;
  __unenv__ = true;
  __unenv__ = true;
    __unenv__: { get: () => true }

参考

Discussion