😊

zenn-validator の紹介

2022/12/16に公開

最近 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 にあります。

https://www.npmjs.com/package/zenn-validator

https://github.com/zenn-dev/zenn-editor/tree/canary/packages/zenn-validator

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]

この拡張子問題については以下の記事に詳しく書かれています。

https://zenn.dev/qnighy/articles/19603f11d5f264

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 を建てました。

https://github.com/zenn-dev/zenn-community/issues/479

内容としては嘘々 ESM ではなく Native ESM[5]か CommonJS としてロードできる形でパッケージを npm に公開してほしい、必要な作業は自分がやる、と言うものです。

この issue を建てる過程で、以下の事項を調査して実現可能性を調べました。

  • Native ESM、CommonJS としてロードできるように変更することがそもそも可能か、どのような変更が必要か
    • Native ESM 化で 2 種類、CommonJS 化で 1 種類のアプローチを検証し、いずれの場合も小さな変更で済むことを確認した
  • 利用者ベースの影響範囲の調査
    • npm に zenn-validator へ依存するパッケージが存在しないことを確認した
    • GitHub のパブリックリポジトリに zenn-validator へ依存するコードが存在しないことを確認した

最終的には Zenn の開発チームとして CommonJS 化の変更を受け入れてくださると言うことになったので、PR を作成しました[6]

https://github.com/zenn-dev/zenn-editor/pull/400

変更は翌日にはマージ・リリースされ、無事 CommonJS としてロードできるようになりました。issue の作成からリリースまでかなりの速度でやっていただきありがたかったです。Zenn の開発チームの方々、特に直接の窓口になってくださった @uttk-dev さんにはこの場を借りてお礼申し上げます。

https://github.com/zenn-dev/zenn-editor/pull/401

私の Zenn の記事を管理するリポジトリで zenn-validator のためだけに導入されていた ESBuild も削除できました。今までありがとう。

Goodbye ESBuild

Future work

さて、こうして素朴な Node.js で動くスクリプトから frontmatter や slug のバリデーションができるようになりました。なったわけですが、まだバリデーションした結果のオブジェクトを TypeScript からうまく扱うことができません。
これはバリデーションされたオブジェクトを表現する型の定義が Zenn CLI の中にはあるものの、外部に公開されていないためです。バリデーション済みのオブジェクトの型を TypeScript の上でうまく扱うためには、以下のように少し手間をかける必要があります。

  • zenn-cli パッケージ内の型定義を手動でコピペ[7]
  • ユーザ定義タイプガードや as などを用いてバリデーションをパスしたオブジェクトの型を上書きする

この手間のうち、少なくとも前者を、できれば後者も zenn-validator でサポートしてほしいと言う issue を建てました。

https://github.com/zenn-dev/zenn-community/issues/480

まだこの issue はどうなるかはわかりませんが、Zenn のエコシステムの体験がもっとよくなることを願っています。できる範囲なら協力は惜しみません。

最後に

以上、zenn-validator の紹介と少しだけ zenn-validator にコントリビュートした話でした。
皆さんもぜひ zenn-validator をお使いください。

脚注
  1. エラーハンドリングや frontmatter のパースは省略している気持ちのコードです。 ↩︎

  2. 正確には日本時間 2022 年 12 月 13 日以前に公開されていた、v0.1.134 未満のパッケージ。 ↩︎

  3. 拡張子まで指定しない import 文も ECMAScript の仕様上は問題ありません。ここでは Node.js によってロードされる ESM に話題を絞り、Node.js の ESM loader がロードできないコードを指して嘘々 ESM と呼びます。https://tc39.es/ecma262/#prod-ModuleSpecifier ↩︎

  4. tsc のことは脇に置いておきます。それはそれ、これはこれです。 ↩︎

  5. ここでは嘘々 ESM に対比して Node.js の ESM loader で実行できる JavaScript を指して Native ESM と呼ぶことにします。 ↩︎

  6. 本筋とは全く関係ないですがキリ番を踏みました。 ↩︎

  7. zenn-cli パッケージには TypeScript の型定義が含まれておらず、バンドルされた JavaScript だけが npm パッケージとして公開されています。そのため require.resolve などでパッケージ内からむりやり型定義を引き出すこともできません。 ↩︎

Discussion