Closed9

TS/Jest/モノリポ、ESM化チャレンジ

ゆうてんゆうてん

markuplint ESM化計画

  • 無事v2.0.0をリリースできたのでESM化(もしくはCJSハイブリッド)を進めたい
  • 既に依存モジュールの一部がESM化が始まっていて最新のバージョンを利用できていない
  • csstreeaccname あたりは、それの機能に依存しているのでわりと深刻な問題
ゆうてんゆうてん
  • v3.0.0 をリリースしたけど、結局ESM化はせず終い。v4.0.0でできたらいいなぁ…と思いながらここも更新していく。
  • csstree解決済み
  • accname は使わなくなったので解決済み
  • ただし、やっぱり他のモジュールはわりとESM化が進んでいてアップデートできていない。
ゆうてんゆうてん

パッケージ毎にひとつずつ始めていく

モノリポなので、パッケージ毎に package.jsontsconfig.json を分けているため(TSは extends で継承しながら)ESM化もパッケージ毎にできるのでは?

ということで、モノリポの中で依存ツリーの最上位になる markuplint からESM化することにする。なぜ最上位からというと、下層から初めてしまうとそれに依存するパッケージもESM化しないといけなくなる(気がする)から。正直ここもわかってない。何も知らない。

ゆうてんゆうてん

設定変更

まずは対象のパッケージの設定を変える。

package.json
{
-   "main": "lib/index.js",
-   "types": "lib/index.d.ts",
+   "type": "module",
+   "main": "lib/index.mjs",
+   "types": "lib/index.d.mts"
}
tsconfig.json
{
  "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 するときに拡張子を読ませる作法か仕様らしく一旦それに従う。

index.ts
export { MLEngine } from './api';
export * from './i18n';
export * from './testing-tool';
export * from './types';

index.mts
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 の警告はひとまず無効化することで対応。

.eslintrc
{
  "rules": {
+    "import/no-unresolved": [
+      "error",
+      {
+        "ignore": ["\\.mjs$"]
+      }
    ]
  }
}
ゆうてんゆうてん

依存パッケージをESM対応の最新にする

いくつかESM化されていたせいで最新にできなかったパッケージを今回は最新にした!(気持ちがいいね!)

そして、ビルドをしたところコンパイル成功🎉

なかなかスムーズにできている気がする。

ゆうてんゆうてん

コマンド実行ファイルの修正

ESMでは拡張子が必須のようで、次のように修正。
require も使えなくなる(工夫すれば使えるらしい)のでDynamic Importで対応。Top Level Awaitが使えるのも嬉しい(ここではあんまり意味ないけど)。

bin/markuplint
#!/usr/bin/env node

require('../lib/cli');

bin/markuplint.mjs
#!/usr/bin/env node

await import('../lib/cli/index.mjs');
package.json
{
  "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;

といった問題が発生していたので修正。

cli/prompt.mts
- 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(/* ...略... */);

パッケージ側の設定の問題かと思ったので、比較してみる。

比較

packages/@markuplint/file-resolver/package.json
{
  "name": "@markuplint/file-resolver",
  "version": "2.0.0",
  "main": "lib/index.js",
  "types": "lib/index.d.ts"
}
packages/@markuplint/create-rule-helper/package.json
{
  "name": "@markuplint/create-rule-helper",
  "version": "2.0.0",
  "main": "lib/index.js",
  "types": "lib/index.d.ts"
}
packages/@markuplint/file-resolver/lib/index.js
"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);
packages/@markuplint/create-rule-helper/lib/index.js
"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化した後に何もなければ忘れることにする。

ゆうてんゆうてん

Jestが *.mts を一切受け付けない

テストができない…。

(つづく)

このスクラップは2ヶ月前にクローズされました