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は、createLinterloadTextlintrcなどがあります。 これらの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メッセージを返します。
      • fixFilesfixText は、ファイルを書き換えるのでなく、書き換えた結果のテキストを含むFixメッセージを返します。
  • loadTextlintrc: .textlintrcの設定ファイルを読み込み、Descriptorという設定オブジェクトを返します。
  • loadLinerFormatter and loadFixerFormatter: それぞれ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 ファイル(拡張子なし) を生成していました
  • @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 の扱いがオプショナルになっていて、意図しない挙動となる可能性がありました
  • 終了ステータスコードを整理しました
    • 今までは 01でした。
    • 0 正常終了、1 Lintエラーがある、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を作って調査していました。

textlintはESLintとは異なり、最初期から非同期処理でLintをするようにデザインしていました。 しかし、設定ファイルやルールのロードは同期処理になっていました。(内部的にrequireを使うため、あえて非同期処理にしないと非同期にできなかったため)

調査してみて、ESMのロードを互換性を持ってやるのがかなり難しいことがわかりました。 最終的にだめだった理由は、availableExtensionsというどのファイルをLintできるかという拡張子を返すAPIが同期的だったためです。 この拡張子はプラグインをロードするまでわからないのですが、ESMで書かれたプラグインをロードするには非同期処理にする必要があります。 そのため、完全に互換性を持ってESMをサポートすることは無理でした。

textlintの内部でロード処理がconfig.tsやTextLintEngineなどに分散していて、それぞれがロード処理を扱っていたのも厄介でした。 そのため、これらの処理を整理して新しいAPIを作り、それを使ってESMをサポートすることにしました。

TextLintEngineでは、次のような流れでLintをしていました。 Engineという部分が、設定ファイルもロードするし、フォーマッターもロードするし、Lintするファイルも探すし、KernelにLintを依頼するという複雑な処理をしていました。

Engine

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

Linter

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ベースで作るか設定したいです。

いくつかやるとルールやプラグインの開発体験が良くなるIssueがあります。

@textlint/kernelをもっと小さくしたいので、いらない依存は削りたいです。