textlint v12.3.0リリース: ESMで書かれたルールを読み込めるように
textlint v12.3.0をリリースして、 ECMAScript Modules(ESM)で書かれたルールやプラグインを扱えるように対応しました。
- Release v12.3.0 · textlint/textlint
- v12時点では、
TEXTLINT_USE_NEW_CLI=1 textlint README.md
またはtextlint-esm README.md
でのみESMを扱えます
- v12時点では、
新しい仕組みには細かな互換性のないところがあるので、v12.3.0の段階ではフラグ付きで利用できるようにしています。 次のメジャーバージョンであるv13で、デフォルトに切り替える予定です。
コマンドラインユーザー向け
textlint
をコマンドラインから利用している場合は、次のように環境変数を指定すると新しい実装に切り替わります。
TEXTLINT_USE_NEW_CLI=1 textlint README.md
または、textlint-esm
というコマンドが増えているので、次のコマンドでも同じ結果になります。
textlint-esm README.md
コマンドラインレベルでは、基本的に新しい実装でも結果は同じになるはずです。 同じ結果にならなかったり、新しい実装にするとエラーが出たら報告してください。
エディタユーザー向け
vscode-textlintなどエディタプラグインは、おそらく古いAPIを使ってる場合があるので、プラグイン開発者の対応が必要です。
もし、プラグイン開発をしている際にtextlint
パッケージを使っていて、新しいAPIだと実装できないケースがあったらおしらせください。
(.textlintrcを読み込まない場合は、@textlint/kernel
パッケージが一番低レベルなAPIなので最適です。)
エディタプラグインでもtextlint
コマンドを使って実装してるケースは、TEXTLINT_USE_NEW_CLI=1
を使って切り替えておかしくないかを確認してください。
基本的にはコマンドレベルでは互換性があるので、多くは問題ないはずです。
新しい実装のtextlint
コマンドでは、--stdin
フラグを使っているときは--stdin-filename=仮想的なファイル名
の指定も必須になっています。
ファイル名を指定しないとファイルの種類(textやMarkdownなど)が判定できなくておかしいので、ちゃんとチェックするように修正しています。
ESMに対応した新しい仕組みを導入した背景
今までのtextlintは、CommonJSで書かれた(CommonJSに変換された)ルールやプラグインしか読み込めませんでした。
CommonJSで書かれたルールやプラグインは、CommonJSのrequire関数で読み込めます。
一方で、ESMで書かれたルールやプラグインはimport()
(Dynamic Import)で読み込む必要があります。
require関数は同期処理なのに対して、import()
は非同期処理です。
そのため、ルールを読み込むところを非同期処理にする必要があり、これはtextlintをモジュールとして使う場合のAPIに影響がありました。
具体的には、TextLintCore
/TextFixEngine
/TextLintEngine
というAPIの中で、.textlintrc
ファイルを読み込んでルールなどをrequire
で読み込んでいました。
今までは同期処理が前提だったので、これらのクラスの初期化処理も同期的なAPIになっていました。
v12.3.0では、これらのAPIの代わりとなるloadTextlintrc
/createLinter
/loadLinterFormatter
/loadFixerFormatter
という新しいAPIを導入しました。
今までのTextLintCore
/TextFixEngine
/TextLintEngine
は、.textlintrc
の読み込みとLintという処理が一緒になっていたクラスでした。
新しいAPIでは、loadTextlintrc
で.textlintrc
を読み込み、その読み込んだ情報を使ってcreateLinter
でLintをするというように、.textlintrc
の読み込みとLintの処理を分離しています。
古いAPIだと次のように書いていました。
import { TextLintEngine } from "textlint";
const engine = new TextLintEngine({
formatterName: "stylish"
});
const results = await engine.executeOnFiles(["*.md"]);
const output = engine.formatResults(results);
console.log(output);
新しいAPIだと次のようにかけます。
import { createLinter, loadTextlintrc, loadLinterFormatter } from "textlint";
const descriptor = await loadTextlintrc();
const linter = createLinter({
descriptor
});
const results = await linter.lintFiles(["*.md"]);
const formatter = await loadLinterFormatter({ formatterName: "stylish" })
const output = formatter.format(results);
console.log(output);
それぞれ、.textlintrc
のロードやformatterのロードなどがLint処理自体を別れるようになっています。
無理やり対応しようと思えば、TextLintEngine
のまま対応もできたのですが、一部のAPIに破綻するものがあったので新しいAPIとして用意することにしました。
具体的には、textlintにはプラグインで対応するファイルの種類を増やせるという仕組みが関係しています。
今までは、engine.availableExtensions
でLintに対応したファイル拡張子の一覧が取得できていました。
import { TextLintEngine } from "textlint";
const engine = new TextLintEngine({
formatterName: "stylish"
});
console.log(engine.availableExtensions); // => [".md", ".txt"]
しかし、この拡張子の一覧は設定ファイルやプラグインなどを読み込まないとわかりません。
require
関数で読み込んでいた場合は同期処理なので成立しましたが、ESMの場合は非同期処理となるためこの一覧は読み込み完了前に決定できません。
対応しているファイルは、.textlintrc
ファイルを読み込んだ時点(プラグインを読み込んだ時点)で決定するため、新しいAPIでは次のようにloadTextlintrc
の結果から対応する拡張子の一覧が取得できます。
Linterは関係なく、設定ファイルを読み込むとその結果からわかるようになっています。
import { createLinter, loadTextlintrc } from "textlint";
const textlintrcDescriptor = await loadTextlintrc();
const availableExtensions = textlintrcDescriptor.availableExtensions;
console.log(availableExtensions); // => [".md", ".txt"]
他にも新しいAPIのサンプルコードは次のページにあるので、textlint
パッケージを使う人は参照してください。
ロードマップ
まだフラグ付きで、新しいAPIを入れた段階です。 そのため、詳細は決まっていませんが、次のような段階を踏んで古いAPIを削除する予定です。
- textlint v12.3.0: 古いAPIをDeprecatedに変更、新しいAPIをUnstableで追加
- 古いAPIがデフォルトです
- CLIも古いAPIを利用します
- textlint v13.0.0: 古いAPIをDeprecatedだけど引き続きサポート、新しいAPIはStableに変更
- 新しいAPIがデフォルトです
- CLIも新しいAPIを利用します
- Ref: textlint v13 roadmap · Issue #902 · textlint/textlint
- textlint v14?: 古いAPIを削除
まとめ
textlint v12.3.0では、ESM対応として新しいAPIを追加しました。 まだフラグ付きですが、ESMで書いたルールやプラグインが動くようになりました。 互換性のためv12では古いAPIをデフォルトにし、v13から新しいAPIがデフォルトとなります。
APIを分けて実装することで、既存の実装と共存して段階的に進んでいけるようになったと思います。
また、古いAPIにはConfig
という変更するのが難しいオブジェクトがありましたが、新しいAPIではConfig
を使わずに済むようになりました。
これは、以前にコア部分をモジュールとして分離していたことで、textlint
の責務を明確にできていたことが大きいかったと思います。
textlint自体のESM化はまだやっていませんが、ブラウザでネイティブに実行できると面白そうなので、今後の課題として考えています。
ただ、ブラウザではtextlint
パッケージではなく@textlint/kernelを扱うようになる気がしています(textlint
はfsを扱うので結局ほとんどが不要)
どちらかというとImport Mapsの対応を考えていくと、ブラウザやDenoで動かすのに関係していきそうな気がします。
構想では.textlintrc
の文字列からesm.shを利用したImport Mapsを動的に生成して、ブラウザでそのままtextlintをimportして動かせるようになるんじゃないかなと思っています。
(.textlintrcが文字列なのは、文字列としてパースしてfsを扱うのを避けるため、Import Mapsを使うのはパッケージのバージョンがtextlintrcには入ってないため)
textlintへのContributeや新しいAPIへのフィードバックを歓迎しています!
Dynamic Importでプラグインの仕組みを実装すると、Windowsで動かないという罠を毎回踏むため、メモを置いておきます。
Windowではファイルパスは C:\\..
のようなパスですが、このファイルパスは import()
ではロードできません。
そのため、import(url.pathToFileURL(filePath).href)
のように File URL(file:///
)に変換してからロードする必要があります。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。