Node.js の ESModules
◆ 今月末の 12 の LTS からフラグはずれるらしい
◆ .cjs/.mjs は拡張子通り
◆ .js は package.json の type で指定した種類のコードとして扱われる
◆ module から cjs は createRequire を使えば .js でも cjs としてロードできる
◆ cjs から module は .mjs でも import 使ってもエラーでダメそう
◆ パッケージルートの参照や package.json の exports キーなどモジュール周りの便利機能が増えてる
◆ .cjs/.mjs は拡張子通り
◆ .js は package.json の type で指定した種類のコードとして扱われる
◆ module から cjs は createRequire を使えば .js でも cjs としてロードできる
◆ cjs から module は .mjs でも import 使ってもエラーでダメそう
◆ パッケージルートの参照や package.json の exports キーなどモジュール周りの便利機能が増えてる
Node.js でも ESModules を使いたいのにいつになったら動きが確定して experimental フラグ外れるんだろうと思って調べてみると 12 の LTS リリース時に外れる予定らしいです
というと今月ですね!
12.11.0 が 9/25 リリースなので 今月末なら次のバージョンの 12.12.0 が LTS になりそうです
安定版の直前かつフラグ外すタイミングで大きな変更はたぶんしないと思うので 今の動きが ESModules の確定した動きと考えていいのかなと思います
ということで ESModules を使ってみました
現状はフラグがいるので すべて --experimental-modules がついてるものとして実行しています
.js の場合は cjs だったり module だったり呼び出し方や設定に応じて変わります
.js を module として扱いたい場合は package.json が必須になりました
package.json に
を書くと .js を module として扱います
なので package.json がないと module として扱えません
個人的に npm ライブラリを使わずちょっとしたコードを書くだけなら package.json は作らないのでちょっと不便です
type 指定をしていなくて module として扱われない場合 基本的に export や import キーワード周りで構文エラーになります
反対に type で module を指定したのに cjs が .js ファイルとしてある場合は module が見つからないなどのエラーになります
どっちかのみならいいのですが 異なるタイプのファイルを .js のみで書いてると困る場合もあります
なので type に module を指定しておいて .js 拡張子の cjs ファイルはこれでロードするのが良さそうです
ただ require は最初から用意されていないので自分で作る必要があります
最近変更があった部分で これまであった createRequireFromPath ではパスを指定していました
module では自身のパスは import.meta.url で URL 形式でしか取得できず 変換が必要だったり使いづらかったのですが createRequire では自分の URL を入れるとこれまでの cjs と同じように使える require 関数を作ることができて扱いやすくなりました
import や export は構文なので cjs 中では使えません
module から cjs をロードするときの require みたいな動的なものとして import 関数があります
しかし これを使ってみると
Error: Not supported
というエラーになってロードできません
.mjs ならできるのかと試してみたら
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module
import を使えと言われました
cjs から module のロードはできなさそうです
将来的な対応はあるかもですが フラグが外れるタイミングでは無理なんじゃないかなと思ってます
ライブラリが module のみ公開だと cjs を使ってるパッケージでは扱えないってことです
同じパッケージ内でも 新規に作るファイルは module にしようということもできません
どうにかしてやれないかなーと思って作ったものがコレです
type を module にして実行します
[main.mjs]
[old.cjs]
cjs のパッケージでモジュール置き場になる esm-modules を用意します
module をロードできる main で必要な module すべてをロードしてここに登録します
終わったら cjs のこれまでの処理を呼び出します
module のライブラリを使いたいときは esm-modules モジュールをロードしてそこからモジュールを取り出します
これは cjs のモジュールなので cjs モジュールからロードできます
これまでのを置き換えるなら まず type を module にして新しい main モジュールを作ります
そこでは上の main.mjs のように module で公開されているライブラリモジュールを登録する処理を行い そのあとで旧 main モジュールを呼び出します
module のライブラリを使いたい cjs ファイルでのみ esm-modules を経由してモジュールを使用します
なんかすごく手間ですね……
準備が手間ではありますが rollup とかで cjs 化してエクスポートするようにしたほうがいいのかも
-e オプションや標準入力で渡したスクリプトなどファイルを使わない実行で使われます
じゃあ 標準入力から読ませればと思って
こうしてみたのですが file.js は module として実行されるのですが file.js がロードする .js ファイルは package.json がないと cjs になってしまってダメでした
また REPL を module にできるのかと思って試してみると
Cannot specify --input-type for REPL
とエラーになりました
今のところ cjs から module をロードできないので REPL で module は使えないということになりますが それって結構不便ですね……
特にファイルをまとめたりして階層をずらした後に修正しようとすると すごく困ります
それがパッケージルートを参照できるようになるようです
そのパッケージの package.json で name に指定した名前を使うと自分自身のパッケージを参照できて node_modules のパッケージのパス指定みたいにパッケージルートからのパスで指定できるようです
たしか parcel だと ~ でパッケージルートを表してますね
proposal には ~ も触れてましたが 名前がメインみたいです
ただ PR では @ を使っていて結局どうなるのか ちゃんと議論を全部追わないとわからない気もします
現在の最新情報だけまとまってればいいのですけどねー
方法は何にしろ とりあえずパッケージルートに楽にアクセスできる便利な機能が追加されるそうです
オブジェクトで指定して キーで指定したパスはバリューで指定したパスに置き換えられます
Node.js のコードでは index.js みたいな全体にアクセスしてプロパティを取得するのが多いですが それだと必要以上のモジュールがロードされる問題もあります
必要なモジュールだけをロードしようとすると パス形式でパッケージルートからのパスを書くことになります
そうすると src とか dist とか lib とかが入ってきて変に長くなります
そういうのを解決するもので package.json にマッピングを書いておくことで 長い部分を省略できるようになります
proposal では exports と imports の 2 つがありますが exports のみ 12.7.0 でフラグ付きで使えるようになってます
imports も exports と同じパスマッピングですが 内部的に使うパスマッピングで外部パッケージからは使えないというものです
imports のマッピングは区別するためか名前が # から始まっています
private property の # から来てそうです
さっそく ESModules を使おうとしたのですが 動きません
ドキュメントを見てみると未だに Experimental のままでした
予定が変わったのかなと調べてみると
https://github.com/nodejs/modules/issues/400
https://github.com/nodejs/modules/issues/408
https://github.com/nodejs/modules/issues/411
延期になったみたいですね
13 のどこかでフラグが外す予定みたいで 問題なければ 12 でも外す案があるみたいですが 13 でどうなるか次第という感じです
12 で使えないと LTS かつ自分でフラグ設定できない実行環境だと来年の 14 を待たないといけなくなりますし 12 でフラグなくしてほしいものです
というと今月ですね!
12.11.0 が 9/25 リリースなので 今月末なら次のバージョンの 12.12.0 が LTS になりそうです
安定版の直前かつフラグ外すタイミングで大きな変更はたぶんしないと思うので 今の動きが ESModules の確定した動きと考えていいのかなと思います
ということで ESModules を使ってみました
現状はフラグがいるので すべて --experimental-modules がついてるものとして実行しています
.cjs と .mjs
拡張子が .cjs や .mjs の場合はそれぞれの拡張子のファイルの扱いでパースされます.js の場合は cjs だったり module だったり呼び出し方や設定に応じて変わります
.js を module として扱いたい場合は package.json が必須になりました
package.json に
{ "type": "module" }
を書くと .js を module として扱います
なので package.json がないと module として扱えません
個人的に npm ライブラリを使わずちょっとしたコードを書くだけなら package.json は作らないのでちょっと不便です
type 指定をしていなくて module として扱われない場合 基本的に export や import キーワード周りで構文エラーになります
反対に type で module を指定したのに cjs が .js ファイルとしてある場合は module が見つからないなどのエラーになります
どっちかのみならいいのですが 異なるタイプのファイルを .js のみで書いてると困る場合もあります
module → cjs
module から cjs をロードする場合は require を使えば .js でも cjs としてロードできますなので type に module を指定しておいて .js 拡張子の cjs ファイルはこれでロードするのが良さそうです
ただ require は最初から用意されていないので自分で作る必要があります
import { createRequire } from "module"
const require = createRequire(import.meta.url)
const cjs = require("./cjs.js")
console.log(cjs)
最近変更があった部分で これまであった createRequireFromPath ではパスを指定していました
module では自身のパスは import.meta.url で URL 形式でしか取得できず 変換が必要だったり使いづらかったのですが createRequire では自分の URL を入れるとこれまでの cjs と同じように使える require 関数を作ることができて扱いやすくなりました
cjs → module
逆の cjs から module のロードですが無理そうですimport や export は構文なので cjs 中では使えません
module から cjs をロードするときの require みたいな動的なものとして import 関数があります
しかし これを使ってみると
Error: Not supported
というエラーになってロードできません
.mjs ならできるのかと試してみたら
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module
import を使えと言われました
cjs から module のロードはできなさそうです
将来的な対応はあるかもですが フラグが外れるタイミングでは無理なんじゃないかなと思ってます
cjs → module をなんとかやる
でもこれって結構困りますよねライブラリが module のみ公開だと cjs を使ってるパッケージでは扱えないってことです
同じパッケージ内でも 新規に作るファイルは module にしようということもできません
どうにかしてやれないかなーと思って作ったものがコレです
type を module にして実行します
[main.mjs]
import { add } from "./esm-modules.cjs"
import foo from "./foo.mjs"
import bar from "./bar.mjs"
import old from "./old.cjs"
add("foo", foo)
add("bar", bar)
old.ready()
old.something()
[old.cjs]
const { get } = require("./esm-modules.cjs")
let foo
let bar
module.exports.ready = () => {
foo = get("foo")
bar = get("bar")
}
module.exports.something = () => {}
cjs のパッケージでモジュール置き場になる esm-modules を用意します
module をロードできる main で必要な module すべてをロードしてここに登録します
終わったら cjs のこれまでの処理を呼び出します
module のライブラリを使いたいときは esm-modules モジュールをロードしてそこからモジュールを取り出します
これは cjs のモジュールなので cjs モジュールからロードできます
これまでのを置き換えるなら まず type を module にして新しい main モジュールを作ります
そこでは上の main.mjs のように module で公開されているライブラリモジュールを登録する処理を行い そのあとで旧 main モジュールを呼び出します
module のライブラリを使いたい cjs ファイルでのみ esm-modules を経由してモジュールを使用します
なんかすごく手間ですね……
準備が手間ではありますが rollup とかで cjs 化してエクスポートするようにしたほうがいいのかも
--input-type
--input-type というオプションもあったので package.json 以外に実行時に指定できるのか と思ったのですが違いました-e オプションや標準入力で渡したスクリプトなどファイルを使わない実行で使われます
> C:\Users\ner>node --experimental-modules --input-type=module -e "console.log(import.meta)"
(node:6592) ExperimentalWarning: The ESM module loader is experimental.
[Object: null prototype] {}
じゃあ 標準入力から読ませればと思って
node --experimental-modules --input-type=module < file.js
こうしてみたのですが file.js は module として実行されるのですが file.js がロードする .js ファイルは package.json がないと cjs になってしまってダメでした
また REPL を module にできるのかと思って試してみると
Cannot specify --input-type for REPL
とエラーになりました
今のところ cjs から module をロードできないので REPL で module は使えないということになりますが それって結構不便ですね……
その他モジュール周り
ESModules に直接関連しなそうですが モジュールいくつか便利な機能が増えるようですパッケージルートの参照
パッケージ内で "../../../" みたいなのが多くなってくると 今の階層がわかりづらくて困ります特にファイルをまとめたりして階層をずらした後に修正しようとすると すごく困ります
それがパッケージルートを参照できるようになるようです
そのパッケージの package.json で name に指定した名前を使うと自分自身のパッケージを参照できて node_modules のパッケージのパス指定みたいにパッケージルートからのパスで指定できるようです
たしか parcel だと ~ でパッケージルートを表してますね
proposal には ~ も触れてましたが 名前がメインみたいです
ただ PR では @ を使っていて結局どうなるのか ちゃんと議論を全部追わないとわからない気もします
現在の最新情報だけまとまってればいいのですけどねー
方法は何にしろ とりあえずパッケージルートに楽にアクセスできる便利な機能が追加されるそうです
exports
package.json のキーに exports が追加されてパスマッピングができるみたいですオブジェクトで指定して キーで指定したパスはバリューで指定したパスに置き換えられます
Node.js のコードでは index.js みたいな全体にアクセスしてプロパティを取得するのが多いですが それだと必要以上のモジュールがロードされる問題もあります
必要なモジュールだけをロードしようとすると パス形式でパッケージルートからのパスを書くことになります
そうすると src とか dist とか lib とかが入ってきて変に長くなります
そういうのを解決するもので package.json にマッピングを書いておくことで 長い部分を省略できるようになります
proposal では exports と imports の 2 つがありますが exports のみ 12.7.0 でフラグ付きで使えるようになってます
imports も exports と同じパスマッピングですが 内部的に使うパスマッピングで外部パッケージからは使えないというものです
imports のマッピングは区別するためか名前が # から始まっています
private property の # から来てそうです
追記
予想は外れて Node.js 12.12 は 10/11 にリリースされて LTS は 12.13 になりましたさっそく ESModules を使おうとしたのですが 動きません
ドキュメントを見てみると未だに Experimental のままでした
予定が変わったのかなと調べてみると
https://github.com/nodejs/modules/issues/400
https://github.com/nodejs/modules/issues/408
https://github.com/nodejs/modules/issues/411
延期になったみたいですね
13 のどこかでフラグが外す予定みたいで 問題なければ 12 でも外す案があるみたいですが 13 でどうなるか次第という感じです
12 で使えないと LTS かつ自分でフラグ設定できない実行環境だと来年の 14 を待たないといけなくなりますし 12 でフラグなくしてほしいものです