zenn-validator の紹介
最近 zenn-validator
という Zenn が出している Zenn の記事の Markdown をバリデーションするパッケージにコントリビュートしました。
その報告がてら zenn-validator
紹介します。
これは 天久保 Advent Calendar 2022 の 15 日目の記事です。
zenn-validator
とは
zenn-validator
は、前述の通り Zenn の記事の Markdown をバリデーションするための npm パッケージです。
わかりやすく言うと Zenn CLI で Zenn の記事の Markdown をプレビューすると出てくるあれのバックエンドです。
実装は zenn-editor/ at canary · zenn-dev/zenn-editor · GitHub にあります。
zenn-validator
の利用
Zenn の GitHub 連携機能を用いて記事を書いていれば、当然の欲求としてこのようなバリデーションを CI やローカルで回したくなるはずです。
API はライブラリとして公開されており、以下のように利用できます[1]。
import fs from "fs/promises";
import path from "path";
import * as validator from "zenn-validator";
import { Dect } from "zenn-validator/lib/types";
type ZennValidationError = validator.ValidationError;
export async function validateZennFrontMatter(
filePath: string,
): Promise<ValidationError[]> {
const content = await fs.readFile(filePath, "utf-8");
const frontmatter = extractFrontMatter(content); // frontmatter の YAML をパースする関数
const article: Dect = {
...frontmatter,
slug: path.basename(filePath, path.extname(filePath)),
};
return validator.validateArticle(article);
}
現在では上記のようなコードを CommonJS をターゲットにして tsc でコンパイルするだけで使用できる zenn-validator
ですが、つい数日前[2]まではそれができませんでした。
JavaScript のモジュールシステム
Node.js のエコシステムでよく使用されるモジュールシステムに、ECMAScript modules(以下 ESM)と CommonJS modules(以下 CommonJS)があります。
そして、TypeScript で素朴にライブラリを書いて公開するとどちらのモジュールとしても読み込めない状態になってしまうことがあります。
TypeScript を書く際は import
文の対象が特定のファイルであっても拡張子は書かないことが一般的です。
import "./path/to/file";
しかし Node.js の ESM loader は import
文に拡張子まで含む完全なパスを書くことを要求します。そして TypeScript Compiler(以下 tsc)は ESM をターゲットにしてトランスパイルしたとき import
文の拡張子を補完しません。そのため、tsc から出力される JavaScript は ESM loader を用いてもそのままでは Node.js で実行できません。このような JavaScript のコードを本記事では嘘々 ESM と呼ぶことにします[3]。
この拡張子問題については以下の記事に詳しく書かれています。
ESM を使う JavaScript として Node.js でそのまま実行できるコードを TypeScript から得るには以下のいずれかが必要になります。いずれのアプローチもパッケージの制作者側で対応するものになります。
- tsc に入力する TypeScript の時点で
js
/mjs
の拡張子を書く - トランスパイラ等を使って
import
文の拡張子を補完する
もちろん、そもそも TypeScript のターゲットを ESM ではなく CJS にするアプローチも考えられます。これもパッケージ制作者側の対応が必要です。
また、嘘々 ESM で公開されたパッケージを利用者側で上手く処理するアプローチとしては以下のようなものが考えられます。
- (利用者側で)トランスパイラ等を使って
import
文の拡張子を補完する - モジュールバンドラでバンドルする
zenn-validator
に話を戻します。
v0.1.134
未満の zenn-validator
は上記の嘘々 ESM の状態で npm に公開されていました。zenn-validator
を利用者として見ると、frontmatter のバリデーションするスクリプトのためだけにトランスパイラやモジュールバンドラを用意する必要があるわけです。小さなスクリプトのためにトランスパイラやモジュールバンドラといった大袈裟な仕組みを導入するのは避けたいのが人情です[4]。
コントリビュート
困ったので issue を建てました。
内容としては嘘々 ESM ではなく Native ESM[5]か CommonJS としてロードできる形でパッケージを npm に公開してほしい、必要な作業は自分がやる、と言うものです。
この issue を建てる過程で、以下の事項を調査して実現可能性を調べました。
- Native ESM、CommonJS としてロードできるように変更することがそもそも可能か、どのような変更が必要か
- Native ESM 化で 2 種類、CommonJS 化で 1 種類のアプローチを検証し、いずれの場合も小さな変更で済むことを確認した
- 利用者ベースの影響範囲の調査
- npm に
zenn-validator
へ依存するパッケージが存在しないことを確認した - GitHub のパブリックリポジトリに
zenn-validator
へ依存するコードが存在しないことを確認した
- npm に
最終的には Zenn の開発チームとして CommonJS 化の変更を受け入れてくださると言うことになったので、PR を作成しました[6]。
変更は翌日にはマージ・リリースされ、無事 CommonJS としてロードできるようになりました。issue の作成からリリースまでかなりの速度でやっていただきありがたかったです。Zenn の開発チームの方々、特に直接の窓口になってくださった @uttk-dev さんにはこの場を借りてお礼申し上げます。
私の Zenn の記事を管理するリポジトリで zenn-validator
のためだけに導入されていた ESBuild も削除できました。今までありがとう。
Future work
さて、こうして素朴な Node.js で動くスクリプトから frontmatter や slug のバリデーションができるようになりました。なったわけですが、まだバリデーションした結果のオブジェクトを TypeScript からうまく扱うことができません。
これはバリデーションされたオブジェクトを表現する型の定義が Zenn CLI の中にはあるものの、外部に公開されていないためです。バリデーション済みのオブジェクトの型を TypeScript の上でうまく扱うためには、以下のように少し手間をかける必要があります。
-
zenn-cli
パッケージ内の型定義を手動でコピペ[7] - ユーザ定義タイプガードや
as
などを用いてバリデーションをパスしたオブジェクトの型を上書きする
この手間のうち、少なくとも前者を、できれば後者も zenn-validator
でサポートしてほしいと言う issue を建てました。
まだこの issue はどうなるかはわかりませんが、Zenn のエコシステムの体験がもっとよくなることを願っています。できる範囲なら協力は惜しみません。
最後に
以上、zenn-validator
の紹介と少しだけ zenn-validator
にコントリビュートした話でした。
皆さんもぜひ zenn-validator
をお使いください。
-
エラーハンドリングや frontmatter のパースは省略している気持ちのコードです。 ↩︎
-
正確には日本時間 2022 年 12 月 13 日以前に公開されていた、
v0.1.134
未満のパッケージ。 ↩︎ -
拡張子まで指定しない
import
文も ECMAScript の仕様上は問題ありません。ここでは Node.js によってロードされる ESM に話題を絞り、Node.js の ESM loader がロードできないコードを指して嘘々 ESM と呼びます。https://tc39.es/ecma262/#prod-ModuleSpecifier ↩︎ -
tsc のことは脇に置いておきます。それはそれ、これはこれです。 ↩︎
-
ここでは嘘々 ESM に対比して Node.js の ESM loader で実行できる JavaScript を指して Native ESM と呼ぶことにします。 ↩︎
-
本筋とは全く関係ないですがキリ番を踏みました。 ↩︎
-
zenn-cli
パッケージには TypeScript の型定義が含まれておらず、バンドルされた JavaScript だけが npm パッケージとして公開されています。そのためrequire.resolve
などでパッケージ内からむりやり型定義を引き出すこともできません。 ↩︎
Discussion