markdownからhtmlに変換したいなーと思い、
micromarkを使おうとしたら、
ES Modulesでかなりハマったので、その時の備忘録。
はじまり
今まで使っていたらちょっとしたツールのプロジェクトに、
micromarkをインストールしたら、こんなエラーが。。
$ npx ts-node sample.ts ./node_modules/ts-node/dist/index.js:842 return old(m, filename); ^ Error [ERR_REQUIRE_ESM]: require() of ES Module ./src/node_modules/micromark/index.js from ./src/sample.ts not supported. Instead change the require of index.js in src/sample.ts to a dynamic import() which is available in all CommonJS modules. at Object.require.extensions.<computed> [as .js] (./node_modules/ts-node/dist/index.js:842:20) at Object.<anonymous> (./src/sample.ts:3:21) at Module.m._compile (./src/node_modules/ts-node/dist/index.js:848:29) at Object.require.extensions.<computed> [as .ts] (./src/node_modules/ts-node/dist/index.js:850:16) at phase4 (./src/node_modules/ts-node/dist/bin.js:414:16) at bootstrap (./src/node_modules/ts-node/dist/bin.js:49:12) at main (./src/node_modules/ts-node/dist/bin.js:32:12) at Object.<anonymous> (./src/node_modules/ts-node/dist/bin.js:526:5) { code: 'ERR_REQUIRE_ESM' }
たしかに、micromarkのREADME.mdには、
This package is ESM only.
と書かれていたので、これっぽい。
tsconfig.json
はこんな感じ。
{ "compilerOptions": { "target": "esnext", "module": "commonjs", "moduleResolution": "node", "lib": ["esnext", "esnext.asynciterable", "dom"], "resolveJsonModule": true, "esModuleInterop": true } }
ES ModulesとCommonJS
ES ModulesとCommonJSは、モジュールシステムらしい。
- ESMはES2015から仕様に入った新しいやつ
- CommonJSはウェブブラウザ環境外を対象にした古いやつ
ざっくりだと、以下のような書き方。
// ESM import { micromark } from "micromark"; // CommonJS const { micromark } = require("micromark");
TypeScriptしか使っていないので、このあたりは気にしたことがなく、
常にimport
を使って書いていたので、気にしたことがなかった。。
このあたりは、以下の記事がとても参考になった。
- CommonJSとES Modulesについてまとめる
- TypeScript 4.7 と Native Node.js ESM | by Yosuke Kurami | May, 2022 | Medium
- TypeScript 4.5 以降で ESM 対応はどうなるのか?
なにがだめだったか
以下の記事でわかりやすくまとめてあるが、
CJSからESMをStatic Importできないことが問題のよう。
TypeScript 4.7 と Native Node.js ESM | by Yosuke Kurami | May, 2022 | Medium
import(require) するファイル import(require) されるファイル Static Import Dynamic Import require ESM ESM OK OK NG CJS CJS NG NG OK ESM CJS OK NG NG CJS ESM NG OK NG
なので、プロジェクト自体をESM化してみた。
ESM対応
ということで、ESM対応をしてみる。以下の記事が参考になった。
・ Pure ESM package
JavaScript(Node.js)側とTypeScript側でそれぞれやることが異なる。
- CommonJSプロジェクトをESMに変更する
- Node.js 14以降をつかう
package.json
に"type": "module"
を追加require()
の排除- importに拡張子を追加
- TypeScriptからESM形式で出力する
- TypeScript 4.7以降を使う
- tsconfig.jsonに
"module": "esnext"
を追加 node --loader ts-node/esm ./my-script.ts
を使って呼び出す
また、ESMにすると、__dirname
や__filename
も使えなくなるので注意。
Node.js / TypeScriptのバージョンアップ
それぞれバージョンが決まっているのでアップデートしておく
- Node.js 14以降をつかう
- TypeScript 4.7以降を使う
package.jsonの変更
ESMのプロジェクトとして認識してもらえるよう、
"type": "module"
を追加。
ついでに、loaderを使って呼び出せるように、
scripts
にもts-esm
を追加。
{ + "type": "module", "scripts": { "ts": "npx ts-node", + "ts-esm": "node --loader ts-node/esm" } }
tsconfig.jsonの変更
TypeScriptからESM形式で出力できるよう、
module
をesnext
に変更する。
(ES2020
でもOK。ES2015
でもよいがimport.meta
など使えない)
{ "compilerOptions": { "target": "esnext", - "module": "commonjs", + "module": "esnext", "moduleResolution": "node", "lib": ["esnext", "esnext.asynciterable", "dom"], "resolveJsonModule": true, "esModuleInterop": true } }
ES2022
やnode12
、node16
、nodenext
などの記載もあるが、
現時点ではnightly buildsのみや対応していない状況っぽい。
・TypeScript: TSConfig Reference - Docs on every TSConfig option
・ESM support: soliciting feedback · Issue #1007 · TypeStrong/ts-node
require()
の排除
require()
はCommonJSの書き方なので、
もし使っていたらimport
形式に書き直す。
ただ、importでは.json
の拡張子を受け付けてくれない。。
import json from "./package.json";
./node_modules/ts-node/dist-raw/node-internal-modules-esm-get_format.js:92 throw new ERR_UNKNOWN_FILE_EXTENSION(ext, fileURLToPath(url)); ^ CustomError: ERR_UNKNOWN_FILE_EXTENSION .json ./package.json ...
なので、createRequireでrequire
を用意する。
import { createRequire } from "module"; const require = createRequire(import.meta.url); const json = require("./package.json");
・How to import JSON files in ES modules (Node.js) | Stefan Judis Web Development
"module"
が見つからないこともあるので、
@types/node
も確認する。v12以降じゃないとだめっぽい。
import { createRequire } from "module";
importに拡張子を追加
ESMの場合、import時に拡張子が必須になる。
なので、内部のファイルには、.js
を追加していく。
- import { util_func } from "./my-module/utils"; + import { util_func } from "./my-module/utils.js";
拡張子がないと、こんな感じでエラーになる。
./node_modules/ts-node/dist-raw/node-internal-modules-esm-resolve.js:366 throw new ERR_MODULE_NOT_FOUND( ^ CustomError: Cannot find module './src/my-module/utils' imported from ./src/sample.ts
拡張子は.ts
であったとしても、.js
で書かないといけない。
.mts
の場合は、.mjs
で書く。
(おまけ) CJS特有機能の置き換え
CJSにしか無い機能の置き換え例。
// require import { createRequire } from "module"; const require = createRequire(import.meta.url);
// __filename / __dirname import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename);
以上!!
たかが、2行のプログラムのために、すごい沼にハマっていった。。(*´ω`*)
import { micromark } from "micromark"; console.log(micromark("## Hello, *world*!")); // => <h2>Hello, <em>world</em>!</h2>
参考にしたサイトさま
- Pure ESM package
- ESM support: soliciting feedback · Issue #1007 · TypeStrong/ts-node
- javascript - Typescript/Node: Error [ERR_MODULE_NOT_FOUND]: Cannot find module - Stack Overflow
- Node.js Dual Packages (CommonJS/ES Modules) に対応した npm パッケージの開発 - Cybozu Inside Out | サイボウズエンジニアのブログ
- Node.js で TypeScript/ESM native な環境構築
- How to Use ECMAScript Modules in Node.js
ts-node
fails when ES Modules are in the dependency graph in Node.js 13+ · Issue #935 · TypeStrong/ts-node- ts-nodeでESModulesのファイルを実行する
- CommonJSとES Modulesについてまとめる
- Node.jsライブラリ/ツールをESMに移行する[Node.js 12+]
- TypeScript 4.7 と Native Node.js ESM | by Yosuke Kurami | May, 2022 | Medium
- TypeScript 4.5 以降で ESM 対応はどうなるのか?
- How to import JSON files in ES modules (Node.js) | Stefan Judis Web Development