本編の 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 は非サポートで見てない。一部のツールが見る可能性がある。
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の非モジュール/モジュール判定は中身を見て判断する。