JSerレポート #2: Node.jsコアモジュールとBundler(webpackなど)によるpolyfillのギャップ

JSerレポート #2: Node.jsコアモジュールとBundler(webpackなど)によるpolyfillのギャップ:

このレポートは、現在進行形で機能追加や仕様変更が行われているNode.jsコアモジュールとブラウザ向けpolyfillにおける挙動の違い(ギャップ)が広がってきている問題について調べたものです。

ここでは https://nodejs.org/api/ に掲載されているうち assertのようにNode.jsにバンドルされているモジュールのことをNode.jsコアモジュールと呼びます。コアモジュールはNode.jsでの利用のみを想定しているため、Node.jsに依存した処理を多く含んでいます。そのため、コアモジュールのコードをコピーしてブラウザなどで動かすことは難しいです。

webpackbrowserifyなどのbundlerは、コード中にあるコアモジュールを代替モジュールへとすり替えます。この代替モジュールはブラウザ向けpolyfillライブラリとよび、このpolyfillライブラリはブラウザで動くようにNode.jsコアモジュールと同等また空のダミー実装をしています。

Node.jsコアモジュールのpolyfillライブラリの例

webpackとbrowserifyは変換時に、コード中に現れるassertモジュールをcommonjs-assertというpolyfillライブラリに自動的にすり替えます。

const assert = require("assert") 
というコードはwebpackなどでbundleすると、次のように書いたのと同じようにモジュールの差し替えが行われます。

const assert = require("commonjs-assert") 
webpackでは、このNode.jsコアモジュールへの差し替えをnodeオプションによって設定が可能です。

polyfill library

webpackとbrowserifyが利用するpolyfillライブラリは次の場所で管理されています。

どちらも基本的に利用しているpolyfill自体はほとんど同じです。

機能のギャップ

このレポートの本題であるNode.jsコアモジュールとブラウザ向けpolyfillのギャップがあったものをまとめた表です。

ここでいうギャップというのは、次のようなケースを並べています。

  • Node.jsコアモジュールで追加されたAPIがpolyfillライブラリには存在しない
  • Node.jsコアモジュールとpolyfillライブラリで挙動が異なる
  • 利用されているpolyfillライブラリがDeprecatedになっている
これらの調査結果については次のリポジトリで管理しています。最新の状況もこのリポジトリに反映しています。

そのため次の表は古くなっている可能性があります。

注記: 依存しているpolyfillそのものはアップデートで解決されている場合があります。しかし、bundlerが古いバージョンを使っている場合があります。

Node.js Browser polyfill Issue Link
assert browserify/commonjs-assert Error code and Error message are different Issue, Article
assert.deepEqual does't support Map, Set, Iterator etc... Issue, Document, Release
require("assert").strict Docs, Release
assert.rejects() Release
assert.doesNotReject() Release
Compatible issue with assert.fail(), assert.ok(), and assert.ifError() No arguments behavior. Release
buffer feross/buffer ---
child_process --- ---
cluster --- ---
console Raynos/console-browserify ---
constants juliangruber/constants-browserify ---
crypto crypto-browserify/crypto-browserify ---
dgram --- ---
dns --- ---
domain bevry/domain-browser ---
events Gozala/events eventNames Issue
getMaxListeners Issue
prependListener Issue
prependOnceListener Issue
off Issue
fs --- ---
http jhiesey/stream-http ---
https substack/https-browserify ---
module --- ---
net --- ---
os CoderPuppy/os-browserify os.constants
path browserify/path-browserify path.posix Issue
path.parse(path) Issue
path.win32
path.format(pathObject)
process defunctzombie/node-process process.channel
process.platform Issue
process.execArgv Issue
process.cpuUsage([previousValue])
process.emitWarning(warning[, options])
punycode bestiejs/punycode.js ---
querystring mike-spainhower/querystring ---
readline --- ---
repl --- ---
stream browserify/stream-browserify ---
string_decoder rvagg/string_decoder TODO Repository
sys defunctzombie/node-util TODO
timers browserify/timers-browserify ---
tls --- ---
tty browserify/tty-browserify ---
url defunctzombie/node-url url.URL(WHATWG URL) Release, Document, Issue
url.format does't support WHATWG URL Release, Document
util defunctzombie/node-util util.callbackify(original)
util.inspect.custom
util.inspect.defaultOptions
util.promisify(original)
util.promisify.custom
util.inspect() options maxArrayLength, breakLength
util.isDeepStrictEqual
util.isDeepStrictEqual
vm browserify/vm-browserify vm.isContext(sandbox)
zlib devongovett/browserify-zlib zlib.bytesRead

実装状況

この調査リポジトリには簡単な機能テストも実装されています。

次にそれぞれでのテスト結果を示します。

Node v11.5.0

24コのテストをすべてパス(これがpolyfillの元なので当然ですが…)

  24 passing (146ms) 

Browserify 16.2.3

4/24のテストをパス。

  gap-test 
    assert 
      1) Error#code 
      2) assert.deepEqual 
      3) assert.strict 
      4) assert.rejects 
      5) assert.doesNotReject 
    events 
      6) off 
      ✓ eventNames 
      ✓ getMaxListeners() 
      ✓ prependListener() 
      ✓ prependOnceListener() 
    os 
      7) constants 
    path 
      8) posix 
      9) win32 
      10) parse 
      11) format 
    process 
      12) platform 
      13) execArgv 
      14) cpuUsage() 
      15) emitWarning() 
    url 
      16) URL 
    util 
      17) inspect.defaultOptions 
      18) callbackify() 
      19) promisify() 
    vm 
      20) isContext 
 
 
  4 passing (293ms) 
  20 failing 

webpack 4.82.2

すべてのテストが失敗しました。

Gapリスト通りのpolyfillが使われています。

  gap-test 
    assert 
      1) Error#code 
      2) assert.deepEqual 
      3) assert.strict 
      4) assert.rejects 
      5) assert.doesNotReject 
    events 
      6) off 
      7) eventNames 
      8) getMaxListeners() 
      9) prependListener() 
      10) prependOnceListener() 
    os 
      11) constants 
    path 
      12) posix 
      13) win32 
      14) parse 
      15) format 
    process 
      16) platform 
      17) execArgv 
      18) cpuUsage() 
      19) emitWarning() 
    url 
      20) URL 
    util 
      21) inspect.defaultOptions 
      22) callbackify() 
      23) promisify() 
    vm 
      24) isContext 
 
 
  0 passing (134ms) 
  24 failing 

おわりに

このレポートは、webpackやbrowserifyを使っているとあまり意識されないpolyfillライブラリに潜在的な問題があることを調べる目的で書きました。この問題の難しさは各polyfillライブラリの管理者やバランスが異なるにもかかわらず、polyfillライブラリ群として暗黙的に参照されている点です。

多くのコアモジュールにおいては、問題が表面化しない可能性もあります。

しかし、asserteventsurlはブラウザ向けとしてよく使われているにもかかわらず、差異が分かる程度にはあります。

また、これらの問題が解決できた場合にも、webpackやbrowserifyには暗黙的にpolyfillライブラリを差し替える仕組み上は、バージョン違いといった互換性の問題が発生するかもしれません。

webpackなどにIssueで同様の問題を報告していましたが、このIssueについては特に進捗はありませんでした。

最近になって、2018年12月21日にwebpack 5 alphaが公開されました。

webpack 5では自動的にNode.jsコアモジュールのpolyfillを自動的に入れないようにする変更が予定されています。

(2018年12月25日時点ではただの予定であるため、該当Issueにおいてフィードバックを求めています。)

In the early days, webpack's aim was to allow running most node.js modules in the browser, but the module landscape changed and many module uses are now written mainly for frontend purposes.

-- https://github.com/webpack/changelog-v5/blob/master/README.md#automatic-nodejs-polyfills-removed
CHANGELOGにこのように書かれているのように、webpackはNode.jsモジュールをブラウザ向けにpack(polyfill)する役割から、フロントエンド向けに書かれたモジュールをbundleする役割へ変わってきています。

今まではBufferなどNode.jsのコアAPIに対応するモジュールを自動的にbundleすることで、Node.js向けに書かれたモジュールをブラウザでも動かせるようにしていました。

一方で、現在ではブラウザ向けに書かれたは多くのモジュールがあるため、webpackが自動的にpolyfillを入れる必然性が小さくなってきています。

また、Bufferのpolyfillなどはファイルサイズがほどほどに大きいため、パフォーマンス面においては自動的にpolyfillを行わないメリットもあります。(polyfillを行うかどうかは、webpack 4でもnodeオプションによって設定が可能です)

少しブラウザとは異なりますが、React NativeのBundlerもNode.jsコアモジュールのpolyfillを自動的に差し替えない仕組みとなっています。

このように、BundlerがNode.jsコアモジュールのpolyfillを暗黙的に入れるという挙動の状況は少し変わりつつあります。

これは、webpack 5の変更予定にも書かれていたように、Bundlerの目的の1つがNode.js向けに書かれたモジュールをブラウザ向けに変換することでした。

しかし、現在は多くのブラウザ向けに書かれたモジュールがあり、Bundlerはそれを効率的に扱うという目的に変わってきている点も関係しているのかもしれません。

jser/report バックナンバー

コメント