◆ 今月末の 12 の LTS からフラグはずれるらしい
◆ .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 がついてるものとして実行しています

.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 でフラグなくしてほしいものです