記事執筆時点での最新版の Node.js では、モジュールシステムとして ES Modules を使うことができる。
また、CommonJS で書かれたモジュールを ES Modules で読み込むこともできる。
Node.js のモジュールシステムは複雑すぎて苦手意識があったので、整理した。
この記事の内容は、Node.js のv14.7.0
で動作確認している。
Node.js のモジュールシステムはバージョン毎に挙動が大きく変わるので、注意が必要。
そのファイルは CJS なのか ESM なのか
Node.js で使えるモジュールシステムとして、ES Modules(以下、ESM)の他に CommonJS(以下、CJS)があり、CJS がデフォルトになっている。
Node.js におけるモジュールシステムを理解するためにはまず、Node.js が各ファイルをどのモジュールシステムとして扱うのかを、理解できるようになる必要がある。
CJS で書かれたファイルとして扱われるのか、ESM で書かれたファイルとして扱われるのかで、挙動が変わるためである。
現状、typeof module
の値で、どちらのモジュールシステムなのかを判断できる。
CJS の場合はobject
に、ESM の場合はundefined
になる。
// CJS console.log(typeof module); // object // ESM console.log(typeof module); // undefined
拡張子による判断
まず、拡張子による判断が行われる。
拡張子が.cjs
のファイルは CJS、.mjs
のファイルは ESM として扱われる。
// ./src/index.cjs console.log(typeof module); // object
// ./src/index.mjs console.log(typeof module); // undefined
そして拡張子が.js
のファイルは、package.json
のtype
フィールドの値によって判断される。
package.json の type による判断
package.json
のtype
フィールドがcommonjs
の場合は、.js
ファイルは CJS として扱われる。
// ./src/index.js console.log(typeof module); // object
commonjs
がデフォルト値なので、type
フィールドが存在しない場合も、CJS となる。
type
フィールドをmodule
にすると、.js
ファイルが ESM として扱われるようになる。
// ./src/index.js console.log(typeof module); // undefined
対象のファイルと同じディレクトリにpackage.json
がない場合は、上位のディレクトリを検索していき、一番近い場所にあるpackage.json
の内容が反映される。
例えば以下のディレクトリ構成の時に、./package.json
のtype
フィールドがmodule
で、./src/utils/package.json
のtype
フィールドがcommonjs
だったとする。
├── package.json └── src ├── index.js └── utils ├── index.js └── package.json
そうすると、各.js
ファイルの実行結果は、次のようになる。
// ./src/index.js // ESM として扱われている console.log(typeof module); // undefined
// ./src/utils/index.js // CJS として扱われている console.log(typeof module); // object
なお、拡張子が.cjs
か.mjs
のファイルについては、type
フィールドの影響は受けない。
ESM 同士の挙動
各ファイルがどちらのモジュールシステムで扱われるのかを理解できたので次は、ファイルのインポートとエクスポートについて見ていく。
まずは、ESM 同士の挙動について。
// ./src/utils/foo.mjs export default 1; export const x = 2;
// ./src/index.mjs import Foo, {x} from './utils/foo.mjs'; console.log(Foo); // 1 console.log(x); // 2
特に問題なく扱える。特定のフラグやオプションが必要になることもない。
拡張子の省略はできない。
// ./src/index.mjs // Error [ERR_MODULE_NOT_FOUND] import Foo, {x} from './utils/foo';
--es-module-specifier-resolution=node
フラグをつけて実行すると、拡張子が省略可能になる。
$ node --es-module-specifier-resolution=node src/index.mjs 1 2
また、このフラグをつけている場合、パスとしてディレクトリを指定することでindex
ファイルを読み込む、ということも可能になる。
// ./src/utils/index.mjs export default 9;
// ./src/index.mjs import Foo from './utils/'; console.log(Foo); // 9
CJS 同士の挙動
次は、CJS 同士の挙動について。
// ./src/utils/bar.cjs module.exports = { y: 8, }
// ./src/index.cjs const Bar = require('./utils/bar.cjs'); console.log(Bar); // { y: 8 }
特に問題ないが、こちらも、拡張子の省略について注意点がある。
.cjs
ファイルの拡張子を省略するとエラーになる。
そのため、上記の./src/index.cjs
を以下のように書き換えると、エラーになってしまう。
// ./src/index.cjs // Error: Cannot find module './utils/bar' const Bar = require('./utils/bar');
CJS として扱われている.js
ファイルを読み込む場合は、拡張子を省略できる。
// ./src/utils/bar.js module.exports = { y: 6, }
// ./src/index.cjs const Bar = require('./utils/bar'); console.log(Bar); // { y: 6 }
ディレクトリを指定することでindex.js
を読み込むこともできる。これも、index.cjs
を読み込もうとするとエラーになるので注意。
// ./src/utils/index.js module.exports = { y: 56, }
// ./src/index.cjs const Bar = require('./utils/'); console.log(Bar); // { y: 56 }
ESM から CJS を読み込む
デフォルトインポートは、問題なく行える。
// ./src/utils/bar.cjs module.exports = { y: 8, }
// ./src/index.mjs import Bar from './utils/bar.cjs'; console.log(Bar); // { y: 8 }
名前付きインポートは、エラーになる。
// ./src/index.mjs // SyntaxError import {y} from './utils/bar.cjs';
これは、CJS から読み込むものはdefault
でラップされるため、このような挙動になる。
// ./src/index.mjs import * as Bar from './utils/bar.cjs'; console.log(Bar); // [Module] { default: { y: 8 } } console.log(Bar.default); // { y: 8 } console.log(Bar.default.y); // 8
但し、標準モジュールは名前付きインポートを行える。
// ./src/index.mjs import {readFile} from 'fs'; console.log(readFile); // [Function: readFile]
CJS から ESM を読み込む
以下の ESM ファイルを、CJS ファイルで読み込んでみる。
// ./src/utils/foo.mjs export default 1; export const x = 2;
まず、ESM ファイルに対してrequire
を実行すると、エラーになる。
// ./src/index.cjs // Error [ERR_REQUIRE_ESM] const Foo = require('./utils/foo.mjs');
CJS ファイルのなかでimport
文を使うと、シンタックスエラーになる。
// ./src/index.cjs // SyntaxError: Cannot use import statement outside a module import {Foo} from './utils/foo.mjs';
但し Dynamic import は実行可能なので、それを使って ESM ファイルを読み込むことができる。
// ./src/index.cjs import('./utils/foo.mjs').then(res => { console.log(res); // [Module] { default: 1, x: 2 } });
スキームが http や https の URL は読み込めない
エラーメッセージにあるように、スキームが file か data の URL しか対応しておらず、それ以外の URL を読み込むことはできない。
// ./src/index.mjs // Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only file and data URLs are supported by the default ESM loader import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.esm.browser.js';