textlint v13.0.0 - ESMで書かれたルールを扱えるように
textlint v13をリリースしました!
textlint v13では、ECMAScript Modules(ESM)で書かれたrulesやpluginsを直接読み込めるようになっています。
また、textlintパッケージ(コマンド)を書き直してたり、内部的に新しいAPIを使うようになっています。
ただし、textlint v12とCLIの互換性はあるはずなので、textlintをコマンドとして使ってるユーザーは特にオプションなどに変更はありません。
Summary
For textlint user
次のコマンドでアップデートするだけです!
npm install textlint@13 --save-dev
# or
yarn install textlint@13 --dev
textlint 13.0.0 は Node.js 16以上を要求します。 そのため、Node.js 14を使っている場合は、Node.js 16以上にアップデートする必要があります。
📝 まだNode.js14でも動きはしますが、minor updatesで動かなくなるケースが出てくると思います。
For textlint rule creator
textlint v13.0.0からは、ESMで書かれたルールを直接読み込めるようになりました。 そのため、ルールをESMで書く場合は、CommonJSにトランスパイルしなくても、textlintのルールとして動くようになりました。
⚠️
vscode-textlintなどのエディタは、CLIではなくtextlintをモジュールとして利用しています。
互換性のために古いAPIもtextlint
v13ではサポートしていますが、古いAPIはESMで書かれたルールをサポートしていません。
そのため、textlintをモジュールとして利用しているツールやエディタなどは、新しいAPIに切り替わるまで、ESMで書かれたルールは利用できません。
For textlint module user
textlintをモジュールとして利用している場合は、新しいAPIを使うようにしてください。
textlintパッケージは、v12.3.0から新しいAPIを提供しています。
新しいAPIは、createLinterとloadTextlintrcなどがあります。
これらのAPIは完全に非同期で動作するようになっていて、ESMで書かれたルールをサポートしています。
古いAPIとしては、TextlintCore, TextLintEngine,
TextFixEngineなどがあります。
これらは、ロード部分が同期処理で、Lint処理が非同期処理になっていました。
ESMをサポートする際に、ロード部分も完全に非同期処理にする必要があったため(Dynamic
Importを使うため)、これらの古いAPIは非推奨となっています。
ただし、v13では、TextlintCore, TextLintEngine,
TextFixEngineの古いAPIも互換性のため残しています。
これらの古いAPIは、v14で削除される予定です。
ℹ️ タイムライン:
- textlint v12.3.0 では、古いAPI(非推奨)と新しいAPIを同時にサポート
- 古いAPIがデフォルトでした
- textlint v13.0.0 では、古いAPI(非推奨)と新しいAPIを同時にサポート
- 新しいAPIがデフォルトです - CURRENT
- textlint v14.0.0 では、古いAPI(非推奨)を削除
- 新しいAPIのみが残ります
| API | Description | Behavior | Target Platform | v13+ |
|---|---|---|---|---|
| cli | Command LIne Interface | Async | Node.js | ✅ (Use createLinter and loadTextlintrc internally) |
| textlint | TextLintCoreのシングルトン | Async | Node.js/CommonJS | ❌ Deprecated |
| TextLintCore | かなり古いAPIです。 @texltint/kernelを使ってください | Node.js/CommonJS | ❌ Deprecated | |
| TextLintEngine | Lint Engine API. It load .textlintrc automaticaly | ◉ Loading is Sync ◉ Linting is Async |
Node.js/CommonJS | ❌ Deprecated |
| TextFixEngine | Fix Engine API. It load .textlintrc automaticaly | ◉ Loading is Sync ◉ Fixing is Async |
Node.js/CommonJS | ❌ Deprecated |
| createLinter/loadTextlintrc | 新しいAPIです。完全に非同期 | ◉ Loading is Async ◉ Linting/Fixing is Async |
Node.js/CommonJS and ESM | ✅ Recommended |
New APIs
新しいAPIの簡単な紹介です。
createLinter:linterオブジェクトを作成します。Linterオブジェクトは次のメソッドを持っています。lintFiles(files): Promise<TextlintResult[]>: ファイルをLintして、Lintメッセージを返します。lintText(text, filePath): Promise<TextlintResult>テキストをLintして、Lintメッセージを返します。fixFiles(files): Promise<TextlintFixResult[]>ファイルをLintして、Fixメッセージを返します。fixText(text, filePath): Promise<TextlintFixResult>テキストをLintして、Fixメッセージを返します。fixFilesとfixTextは、ファイルを書き換えるのでなく、書き換えた結果のテキストを含むFixメッセージを返します。
loadTextlintrc:.textlintrcの設定ファイルを読み込み、Descriptorという設定オブジェクトを返します。loadLinerFormatterandloadFixerFormatter: それぞれLinterとFixerのフォーマッタを読み込みます
Examples
MarkdownファイルをLintとして結果をコンソールに出力する例です。
import { createLinter, loadLinterFormatter, loadTextlintrc } from "textlint";
// descriptor is a structure object for linter
// It includes rules, plugins, and options
const descriptor = await loadTextlintrc();
const linter = createLinter({
descriptor,
});
const results = await linter.lintFiles(["*.md"]);
// textlint has two types formatter sets for linter and fixer
const formatter = await loadLinterFormatter({ formatterName: "stylish" });
const output = formatter.format(results);
console.log(output);
より詳しいサンプルや説明は、次のドキュメントを参照してください。
新しいAPIへのフィードバックは、次のDiscussionに書き込んでください。
ChangeLog
ここからは、v13.0.0の変更点です
🔥 Breaking Changes
- Node.js 16+が必要になりました
textlint --initは.textlintrc.jsonを生成するようになりました- v12までは
textlint --initは.textlintrcファイル(拡張子なし) を生成していました
- v12までは
@textlint/ast-node-typesの型定義が改善されました- 今までは、TxtNode/TxtParentNode/TxtTextNodeの型定義のみが存在していました
TxtPragraphNodeなどの個別のNodeの型定義も追加しました- 合わせて、
TxtTableNodeも追加しています - 詳しくはTxtAST Interfaceと
@textlint/ast-node-typesを参照してください
- New-CLIがデフォルトとなり、Old-CLIは削除されました
- New-CLIはv12.3.0でopt-intで導入されました。
- New-CLIは
createLinter/loadTextlintrc/loadLinterFormatter/loadFixerFormatterといった新しいAPIを利用します - これは、ESMのルールやプラグインをサポートするための変更です
- Old-CLIは不要となったため、削除されました
Old-CLI と New-CLI の違い
- New CLI は ESM rules/plugins をサポートしています。
- New CLI では
--stdinを指定するときに--stdin-filenameが必須となりました- Old CLIでは
--stdin-filenameの扱いがオプショナルになっていて、意図しない挙動となる可能性がありました
- Old CLIでは
- 終了ステータスコードを整理しました
- 今までは
0か1でした。 0正常終了、1Lintエラーがある、2クラッシュなどの異常終了といったように整理しています- 詳細はCommand Line Interface · textlintを参照してください
- 今までは
🆕 Features
個別のNodeの定義とTable/TableRow/TableCell Nodeの追加
- 新しくNodeの型定義を
@textlint/ast-node-typesに追加しました - Table/TableRow/TableCell node の定義を
@textlint/ast-node-typesに追加しました
型定義は、@textlint/ast-node-typesから利用できます。
| Type name | Node type | Description |
|---|---|---|
| ASTNodeTypes.Document | TxtDocumentNode(TxtParentNode) | Root Node |
| ASTNodeTypes.DocumentExit | TxtDocumentNode(TxtParentNode) | |
| ASTNodeTypes.Paragraph | TxtParagraphNode(TxtParentNode) | Paragraph Node |
| ASTNodeTypes.ParagraphExit | TxtParagraphNode(TxtParentNode) | |
| ASTNodeTypes.BlockQuote | TxtBlockQuoteNode(TxtParentNode) | > Block Quote Node |
| ASTNodeTypes.BlockQuoteExit | TxtBlockQuoteNode(TxtParentNode) | |
| ASTNodeTypes.List | TxtListNode(TxtParentNode) | List Node |
| ASTNodeTypes.ListExit | TxtListNode(TxtParentNode) | |
| ASTNodeTypes.ListItem | TxtListItemNode(TxtParentNode) | List (each) item Node |
| ASTNodeTypes.ListItemExit | TxtListItemNode(TxtParentNode) | |
| ASTNodeTypes.Header | TxtHeaderNode(TxtParentNode) | # Header Node |
| ASTNodeTypes.HeaderExit | TxtHeaderNode(TxtParentNode) | |
| ASTNodeTypes.CodeBlock | TxtCodeBlockNode(TxtParentNode) | Code Block Node |
| ASTNodeTypes.CodeBlockExit | TxtCodeBlockNode(TxtParentNode) | |
| ASTNodeTypes.HtmlBlock | TxtHtmlBlockNode(TxtParentNode) | HTML Block Node |
| ASTNodeTypes.HtmlBlockExit | TxtHtmlBlockNode(TxtParentNode) | |
| ASTNodeTypes.Link | TxtLinkNode(TxtParentNode) | Link Node |
| ASTNodeTypes.LinkExit | TxtLinkNode(TxtParentNode) | |
| ASTNodeTypes.Delete | TxtDeleteNode(TxtParentNode) | Delete Node(~Str~) |
| ASTNodeTypes.DeleteExit | TxtDeleteNode(TxtParentNode) | |
| ASTNodeTypes.Emphasis | TxtEmphasisNode(TxtParentNode) | Emphasis(*Str*) |
| ASTNodeTypes.EmphasisExit | TxtEmphasisNode(TxtParentNode) | |
| ASTNodeTypes.Strong | TxtStrongNode(TxtParentNode) | Strong Node(**Str**) |
| ASTNodeTypes.StrongExit | TxtStrongNode(TxtParentNode) | |
| ASTNodeTypes.Break | TxtBreakNode | Hard Break Node(Str<space><space>) |
| ASTNodeTypes.BreakExit | TxtBreakNode | |
| ASTNodeTypes.Image | TxtImageNode | Image Node |
| ASTNodeTypes.ImageExit | TxtImageNode | |
| ASTNodeTypes.HorizontalRule | TxtHorizontalRuleNode | Horizontal Node(---) |
| ASTNodeTypes.HorizontalRuleExit | TxtHorizontalRuleNode | |
| ASTNodeTypes.Comment | TxtCommentNode | Comment Node |
| ASTNodeTypes.CommentExit | TxtCommentNode | |
| ASTNodeTypes.Str | TxtStrNode | Str Node |
| ASTNodeTypes.StrExit | TxtStrNode | |
| ASTNodeTypes.Code | TxtCodeNode | Inline Code Node |
| ASTNodeTypes.CodeExit | TxtCodeNode | |
| ASTNodeTypes.Html | TxtHtmlNode | Inline HTML Node |
| ASTNodeTypes.HtmlExit | TxtHtmlNode | |
| ASTNodeTypes.Table | TxtTableNode | Table node. textlint 13+ |
| ASTNodeTypes.TableExit | TxtTableNode | |
| ASTNodeTypes.TableRow | TxtTableRowNode | Table row node. textlint 13+ |
| ASTNodeTypes.TableRowExit | TxtTableRowNode | |
| ASTNodeTypes.TableCell | TxtTableCellNode | Table cell node. textlint 13+ |
| ASTNodeTypes.TableCellExit | TxtTableCellNode |
今までと同じくベースとなるのはTxtNode/TxtParentNode/TxtTextNodeの3つのNodeです。
今回<h1>のようなHeaderならTxtHeaderNodeといったように個別のNodeを追加しています。
これらのNodeはTxtNode/TxtParentNode/TxtTextNodeのどれかを継承していますが、Nodeごとに特有のプロパティを持ってる場合があります。
TxtHeaderNodeならlevelというプロパティを持っています。
export interface TxtHeaderNode extends TxtParentNode {
type: "Header";
depth: 1 | 2 | 3 | 4 | 5 | 6;
children: PhrasingContent[];
}
詳しくは@textlint/ast-node-typesを参照してください。
Full Changelog
v13をリリースするまでの流れ
最初にESMをサポートするためのIssueを立てて、実際にどうやるかをPoC Pull Requestを作って調査していました。
- Supports rules that are written as ECMAScript Modules · Issue #868 · textlint/textlint
- feat(textlint): support ESM rule/filter-rule/preset/plugin by azu · Pull Request #898 · textlint/textlint
textlintはESLintとは異なり、最初期から非同期処理でLintをするようにデザインしていました。
しかし、設定ファイルやルールのロードは同期処理になっていました。(内部的にrequireを使うため、あえて非同期処理にしないと非同期にできなかったため)
調査してみて、ESMのロードを互換性を持ってやるのがかなり難しいことがわかりました。 最終的にだめだった理由は、availableExtensionsというどのファイルをLintできるかという拡張子を返すAPIが同期的だったためです。 この拡張子はプラグインをロードするまでわからないのですが、ESMで書かれたプラグインをロードするには非同期処理にする必要があります。 そのため、完全に互換性を持ってESMをサポートすることは無理でした。
textlintの内部でロード処理がconfig.tsやTextLintEngineなどに分散していて、それぞれがロード処理を扱っていたのも厄介でした。
そのため、これらの処理を整理して新しいAPIを作り、それを使ってESMをサポートすることにしました。
TextLintEngineでは、次のような流れでLintをしていました。 Engineという部分が、設定ファイルもロードするし、フォーマッターもロードするし、Lintするファイルも探すし、KernelにLintを依頼するという複雑な処理をしていました。

新しいAPIでは、createLinterやloadTextlintrcといったように、それぞれの処理を分離しています。
そのため、ロードはloadTextlintrcで行い、createLinterでファイルの探索をして、LintはKernelに依頼するというように分離しています。

Linterからロードする処理が綺麗に切り離せたと思います。
この辺の設定は、次のIssueにメモをしながら進めていました。
実際にやるとスッキリしましたが、既存のものからどうマイグレーションするかが課題となりました。
CLIのユーザーは、CLIが使うAPIを切り替えればいいだけですが、textlintをモジュールとして利用しているユーザーはAPI自体を変更する必要があります。
CLIでは、Old-CLIとNew-CLIの同じインターフェースと同じテストケースを使って、TEXTLINT_USE_NEW_CLI=1のフラグで切り替える状態にしてv12.3.0をリリースしました。
モジュールの場合は、それぞれのAPIは共存できるので、古いAPIを非推奨として、新しいAPIへと移行を促す方針にしました。 そのため、v13では両方のAPIをサポートし、v14で古いAPIを削除するという方針になっています。
新しいAPIのcreateLinterはすでにあった@textlint/kernelというコアロジックの薄いラッパーです。
このリファクタリングをやる前に、コアを@textlint/kernelに分離していなかったら、あまりにも変更量が多くて、おそらくできなかった変更な気がします。
ちゃんとモジュールを分けておくのは大事だなーと思いました。
あと、コアを分離したときにtextlintでは、CLIという高レイヤーに対するテストを結構増やしていました。 これは、関数に対するユニットテストだと、こういった変更でテストを捨てないといけない場合が多いです。 一方で、CLIのインターフェースはそんなに変わらないため、テストが再利用しやすくて、実際のユーザーに近いテストになります。
同じような理由で、実際のtextlintを使ってるリポジトリを使ったテストもCIで回しています。
実際にこのテストは5年前のリポジトリでも、textlintをアップデートするだけでそのままテストが通ることを確認できています。
これらのテストがあったので、結構安心してリファクタリングできました。
Thanks for Support!
ContributionやGitHub Sponsorsでのサポートありがとうございます!
もし、GitHub Sponsorsでのサポートを検討している場合は、@azu on GitHub Sponsorsをチェックしてください!
textlintへのContributeも歓迎しています。 ウェブサイトやデザインからバグ修正や機能追加など色々やるべきIssueがあります!
ウェブサイトをdocusaurus v2にアップデートしたいです。 あと、OGPがちゃんと設定されてないので、textlint/mediaベースで作るか設定したいです。
- Update to docusaurus@2 · Issue #942 · textlint/textlint
- OGP for website · Issue #864 · textlint/textlint
いくつかやるとルールやプラグインの開発体験が良くなるIssueがあります。
- feat(textlint-tester): add description field · Issue #1005 · textlint/textlint
- ast-tester should validate individual Node type · Issue #1009 · textlint/textlint
@textlint/kernelをもっと小さくしたいので、いらない依存は削りたいです。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。