モジュールシステムの錯綜

本編の JavaScriptは2つの言語になったんだね - 檜山正幸のキマイラ飼育記 (はてなBlog), CommonJSは遠からず消滅するだろう - 檜山正幸のキマイラ飼育記 (はてなBlog) でも書いたが、現状のモジュールシステムは錯綜していて鬱陶しい。将来的にはESM方式に統合されるだろうが。

CommonJS方式はNode.js由来なので、ブラウザとは相性が悪く、そのギャップを埋めるためにツールの山が築かれたわけだ。NPMはNode.jsと密結合しているから、CommonJS は package.json に依存したりする。もちろんブラウザでは package.json 相当のものはない。

以下、モジュールは、なんらかの方法(拡張子、export/importの有無、メタデータなど)でモジュールだとマークされたファイルの意味だとする。モジュール名とはファイル名から拡張子を除外した名前、モジュール・パスはモジュール名にならかの位置修飾子を付けた、ときに拡張子も付けたもの。以上の概念はブラウザでも意味を持つ。

NPM/Node.js だと非モジュールファイル/モジュールファイル以外に、パッケージ概念がある。ブラウザにはパッケージ概念はない。パッケージ名を指定してのモジュールロード(CommonJS では require)が可能。package.json に次を書く。

{
  "name": "パッケージ名",
  "main": "モジュールファイル名"
}

require("パッケージ名")require("モジュールファイルのパス") のように解釈される。

ESMエミュレータとしてのバンドラー

一部のバンドラーは、ユーザーに(構文的には)ESM環境を提供する。構文はESM構文で書けて、メカニズムはCommonJS方式を利用する。

モジュールファイルのexport文、import文はESM構文でありながら、import { ☓☓☓ } from "パッケージ名" を利用可能とする。パッケージ名 → モジュールファイル名 のマッピングに package.json の name, main フィールドを見に行く。

もともとパッケージ概念を持たないブラウザ向けコードをかくときに、パッケージのインポートを利用可能とするのは混乱するだけだと思うが‥‥

exportsフィールド

mainフィールドには単一のモジュールファイルを指定できるだけなので、ESM構文での名前指定のimport文の機能を十分には利用できない。mainフィールドに変わる機能を exportsフィールドが提供する。

が、ブラウザ向けではパッケージ概念を使うべきではないだろう。あくまで、Node.jsのライブラリ利用を、ESM構文の import で行いときだろう。

moduleフィールド

これは、Node.js は非サポートで見てない。一部のツールが見る可能性がある。

https://esbuild.github.io/api/#main-fields に説明がある。

typeフィールドとtypesフィールド

package.jsonのtypeフィールドは、そのスコープ内の.jsファイルを、非モジュールとみなすかモジュールとみなすかを指示する。"type": "module" で、.js は .mjs 扱いになる。

typesはこれとは関係なくて、TypeScriptの型定義の指定。types = typings (別名で)で、パッケージで使う型定義ファイル(d.ts ファイル)のパスを値に指定する。

ブラウザの埋め込みコードと、拡張子が.jsのファイル内コードは事情が似ていて、中身を見ないと非モジュールかモジュールか分からない。そこで:

  • ブラウザの埋め込みコード: scriptタグに、type="module" 属性を指定する。
  • ファイル内コード: package.json に "type": "module" フィールドを追加する。

TypeScriptの非モジュール/モジュール判定は中身を見て判断する。