🗂

コンポーネントライブラリの全Propsを取得・表示して幸せになろう

2021/12/15に公開

この記事はSmartHR Advent Calendar 2021 15日目の記事です。

こんにちは、@kgsiです。久しぶりの技術記事です。
今回は小ネタとして、コンポーネントライブラリのPropsをすべてJsonに出力することで幸せになるための方法を書きました。

何でやるのか?

所属している会社ではSmartHR UIという、React製コンポーネントライブラリがプロダクトで活発に利用され、それ故にリリースサイクルも速く、毎週一回のペースで更新されています。
他方、SmartHR UIの使い方やPropsを定義する場所としてSmartHR Design Systemという、デザインに関わるドキュメントを包括管理しているガイドラインが用意されています。棲み分けと役割を明確に分けており、両者は別々のリポジトリで管理されている状態です。

さて、そんなコンポーネントライブラリとドキュメントシステムを連携させるための課題として、コード上の変更を検知しにくい、煩雑で辛い...というものが挙げられます。

type IColumnProps = {
  /** prop1 description */
  prop1?: string;
  /** prop2 description */
  ...
}

例えばコード側にこういったProps定義があり、どのように挙動するか、値を返すのかをコメントしていますが、コードが変更された後にドキュメントを手で更新するのは煩雑さの極みです、爆発試算してしまいます。
そのため、正確且つ低コストで整備するため、Propsの動的取得・表示させる処理を試みるのでした。

技術的な話

結論から話してしまうと、react-docgen-typescriptを使うことでコンポーネントのPropsの出力が可能です。簡単ですね!

https://github.com/styleguidist/react-docgen-typescript

ちなみに、Storybook用に出力する場合は専用Pluginが既に用意されているので、そちらを使ってみると良さそうです。

https://storybook.js.org/addons/storybook-addon-react-docgen

今回は独自のドキュメントシステムで、すべてコンポーネントを一気に出力させたいため、若干の工夫が必要となります。ポイント別に解説します。

  1. node_modules配下の.tsファイルからpropsを抽出する
  2. 不要な情報をフィルタリング
  3. 探索の自動化・探索できないコンポーネント名を指定

1.node_modules配下の.tsファイルからpropsを抽出する

インストールされたライブラリに出力されている型定義ファイル**.d.tsに絞り、読み込んでいきます。通常TSに対応したライブラリであれば型定義は提供されているはずなので、ディレクトリを設定さえできれば抽出可能です。この事例では、globmoduleを利用して一括で読み込み、ひとつひとつ処理しています。

import * as fs from 'fs'
import * as path from 'path'
import * as glob from 'glob'

const SRC_PATH = path.join(__dirname, './node_modules/smarthr-ui/lib/components/**/**.d.ts')

const options = {
  ...後述する設定を記述
}
const customCompilerOptionsParser = docgen.withCompilerOptions({ esModuleInterop: true }, options)

glob.sync(SRC_PATH).forEach((filename: string): void => {
  const hasTypeFile = componentsNames.some((componentName) => {
    return !filename.indexOf(`${componentName}.d.ts`)
  })
  if (hasTypeFile) {
      const parseComponentDocs = customCompilerOptionsParser.parse(filename)
    // 型定義ファイルにのみreact-docgen-typescriptで処理を実行し、加工する
  }
})

データとして取得したら、あとは思いのままです。SmartHR Design Sysmtemでは、Propsエリアの表示に合わせたJSONを生成し、Gatsbyを通して出力させています。

2.不要な情報をフィルタリング

そのままreact-docgen-typescriptを使うだけでも値を取り込むことは可能ですが、素の状態で取り込むと、Props以外の不要な値も取得されます。もちろん取得後にフィルタリングしても良いですが、ケースによっては大量のデータが出力されてしまうため、事前に絞り込みたい...
解決策としては、標準で用意されているOptionの値とPropsFilterを使うことで、絞り込むことができます。

const options = {
  shouldExtractValuesFromUnion: true,
  savePropValueAsString: false,
  propFilter: {
    skipPropsWithName: ['as', 'id', 'inputMode', 'is'], // 隠蔽要素などの不要なTypeの読み込みをSkip
    skipPropsWithoutDoc: false, // descriptionが書かれていないコンポーネントのPropsをスキップするが、書かれていない場合もあるのでfalseとする
  },
}

3.探索の自動化・探索できないコンポーネント名を指定

対象の型ファイルは再帰的に探索して取得します。ただし、探索ファイルに含められないケースもあるため(複数のファイルが同一ディレクトリに入っていたり、探索名とファイル名とが異なっているなど)、その場合は手動でファイル指定する設定を追加します。

import * as fs from 'fs'
import * as path from 'path'

const componentsPath = path.join(__dirname, './node_modules/smarthr-ui/lib') // 起点となるPath取得
const componentsNames: string[] = []

const existsDir = (dirents: fs.Dirent[]) => dirents.some((dirent) => dirent.isDirectory())

const readRecursively = (dirPath: string, name: string) => {
  const pathName = `${dirPath}/${name}`
  const dirents = fs.readdirSync(pathName, { withFileTypes: true })

  if (existsDir(dirents)) {
    dirents.forEach((dir) => {
      if (dir.isDirectory()) {
        readRecursively(pathName, dir.name) // ディレクトリの場合はディレクトリ名を使って探索を再実行
      }
    })
  } else {
    componentsNames.push(name)
  }
}

readRecursively(componentsPath, 'components')

export default [
  ...componentsNames,
  // Memo: Pathの自動取得では担保できないComponent名を設定する
  'Input',
  ...
]

終わりに

出力されたファイルのサンプルとしてはSmartHR Design Systemをご覧ください。

https://smarthr.design/products/components/button/#h2-5

快適なドキュメント整備のために自動化対応するのは、殆どの場合リターンに見合うと思います。日々の運用をらくにする方法を考えていきたいですね。

Discussion