TS/Jest/モノリポ、ESM化チャレンジ
markuplint ESM化計画
- 無事v2.0.0をリリースできたのでESM化(もしくはCJSハイブリッド)を進めたい
- 既に依存モジュールの一部がESM化が始まっていて最新のバージョンを利用できていない
-
csstree
やaccname
あたりは、それの機能に依存しているのでわりと深刻な問題
パッケージ毎にひとつずつ始めていく
モノリポなので、パッケージ毎に package.json
や tsconfig.json
を分けているため(TSは extends
で継承しながら)ESM化もパッケージ毎にできるのでは?
ということで、モノリポの中で依存ツリーの最上位になる markuplint
からESM化することにする。なぜ最上位からというと、下層から初めてしまうとそれに依存するパッケージもESM化しないといけなくなる(気がする)から。正直ここもわかってない。何も知らない。
設定変更
まずは対象のパッケージの設定を変える。
{
- "main": "lib/index.js",
- "types": "lib/index.d.ts",
+ "type": "module",
+ "main": "lib/index.mjs",
+ "types": "lib/index.d.mts"
}
{
"compilerOptions": {
+ "module": "NodeNext",
+ "target": "ES2020",
"composite": true,
"outDir": "./lib",
"rootDir": "./src"
}
}
VS Code が「"module": "NodeNext"
は安定してないから TypeScript@next をインストールしろ」と警告してくるのでそこもインストールしなおす。
$ yarn add -DW typescript@next
※モノリポのため -W
オプションが必要
確認時点で typescript@4.6.0-dev.20220211
が入った。
しかし、警告は消えない。一旦ここは無視する。
.ts
ファイルを .mts
に置換
とりま VS Code のエクスプローラー(通常左にあるファイルツリーペイン)から、愚直に拡張子を書き換えていく。そうすると VS Code が気を利かせて、相対的に import しているパスを勝手に .mjs
に書き換えてくれる(なんて便利な!!)。
ESM では、ESM のファイルは .mjs
を、CommonJS のファイルは .cjs
として、それを import するときに拡張子を読ませる作法か仕様らしく一旦それに従う。
export { MLEngine } from './api';
export * from './i18n';
export * from './testing-tool';
export * from './types';
↓
export { MLEngine } from './api/index.mjs';
export * from './i18n.mjs';
export * from './testing-tool/index.mjs';
export * from './types.mjs';
う〜ん、index
ってファイル辞めようかなぁって感じてくるなァ。
ここで拡張子が変わったパスに対して ESLint が警告を出してくる。標準ではなく eslint-plugin-import
というのを入れているため import/no-unresolved
というルールが実体ファイルを見つけられずに警告を出してしまったようだ。出力先のフォルダが異なるので当然。しかし TS コンパイラは .mjs
を .mts
と解釈して型チェックを行うため TS 側でのエラーにはならない。慣れるまで気持ち悪いけど我慢が必要っぽい。 ESLint の警告はひとまず無効化することで対応。
{
"rules": {
+ "import/no-unresolved": [
+ "error",
+ {
+ "ignore": ["\\.mjs$"]
+ }
]
}
}
依存パッケージをESM対応の最新にする
いくつかESM化されていたせいで最新にできなかったパッケージを今回は最新にした!(気持ちがいいね!)
そして、ビルドをしたところコンパイル成功🎉
なかなかスムーズにできている気がする。
コマンド実行ファイルの修正
ESMでは拡張子が必須のようで、次のように修正。
require
も使えなくなる(工夫すれば使えるらしい)のでDynamic Importで対応。Top Level Awaitが使えるのも嬉しい(ここではあんまり意味ないけど)。
#!/usr/bin/env node
require('../lib/cli');
↓
#!/usr/bin/env node
await import('../lib/cli/index.mjs');
{
"bin": {
- "markuplint": "bin/markuplint"
+ "markuplint": "bin/markuplint.mjs"
}
}
Named Export 問題
さて、ためしに実行してみる
import { prompt } from 'enquirer';
^^^^^^
SyntaxError: Named export 'prompt' not found. The requested module 'enquirer' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:
import pkg from 'enquirer';
const { prompt } = pkg;
といった問題が発生していたので修正。
- import { prompt } from 'enquirer';
+ import enquirer from 'enquirer';
+
+ const { prompt } = enquirer;
しかし、、、
import { createRuleHelper } from '@markuplint/create-rule-helper';
^^^^^^^^^^^^^^^^
SyntaxError: Named export 'createRuleHelper' not found. The requested module '@markuplint/create-rule-helper' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:
import pkg from '@markuplint/create-rule-helper';
const { createRuleHelper } = pkg;
これって、モノリポの中の依存パッケージも全部これに修正しなきゃいけないってことじゃね??
全部 ESM 化する予定なのに、
import pkg from '@markuplint/create-rule-helper';
const { createRuleHelper } = pkg;
この書き方に一時的に直すの辛いんですけど…🥺
…と思ったら、これが必要なのは @markuplint/create-rule-helper
だけで、他の @markuplint/file-resolver
などは普通に Named Export できてる。…????…意味がわからない…。
しかも、以下のように * as
で対応しないとダメだった。
import * as createRuleHelper from '@markuplint/create-rule-helper';
// 略
createRuleHelper.createRuleHelper(/* ...略... */);
パッケージ側の設定の問題かと思ったので、比較してみる。
比較
{
"name": "@markuplint/file-resolver",
"version": "2.0.0",
"main": "lib/index.js",
"types": "lib/index.d.ts"
}
{
"name": "@markuplint/create-rule-helper",
"version": "2.0.0",
"main": "lib/index.js",
"types": "lib/index.d.ts"
}
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
tslib_1.__exportStar(require("./config-provider"), exports);
tslib_1.__exportStar(require("./resolve-files"), exports);
tslib_1.__exportStar(require("./resolve-parser"), exports);
tslib_1.__exportStar(require("./resolve-rules"), exports);
tslib_1.__exportStar(require("./resolve-specs"), exports);
tslib_1.__exportStar(require("./types"), exports);
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
(0, tslib_1.__exportStar)(require("./create-rule-helper"), exports);
(0, tslib_1.__exportStar)(require("./types"), exports);
なにが問題になってるんだろ…。
未解決
ただ、実行できるようにはなったので、すべてESM化した後に何もなければ忘れることにする。
*.mts
を一切受け付けない
Jestが テストができない…。
(つづく)