[キャッチアップ] react-docgen
スクラップを記事化したのでそっち見てね
本スクラップについて
大好きな Storybook でも使用されている、React コンポーネントのメタデータを収集するツールである react-doc-gen
これの理解を深めれば、Storybook をさらに活用できたり、あるいはツール単体で利用して開発の効率化が出来るのではと思い、適当に深掘って見ることに。
公式ドキュメントをチェックする
最新版である v6 からはドキュメントサイトが新設されている。が、中身はまだスカスカで不十分な感じ。必要そうなところから作成されていると考えれば、作成されてる箇所だけで有益な情報が得られるとも言える。
公式ドキュメント > Home
-
react-docgen
は React コンポーネントからその情報を抽出し、マシンリーダブルな形式で出力するライブラリ - Babel を用いてパースされたAST を使用する
- 出力形式は JSON または JavaScript オブジェクトとなる
- クラスコンポーネントまたは関数コンポーネントの両方に対応
公式ドキュメント > Playground
百聞は一見にしかずで、Reactコンポーネントのコードを貼るとそのメタデータを出力してくれるプレイグラウンドが用意されている。
input
interface Props {
/** The name to greet */
name: string
}
/**
* Hello world component
*/
const MyComponent = ({name = 'world'}: Props) => {
return <div />;
}
output
[
{
"description": "Hello world component",
"displayName": "MyComponent",
"methods": [],
"props": {
"name": {
"required": false,
"tsType": {
"name": "string"
},
"description": "The name to greet",
"defaultValue": {
"value": "'world'",
"computed": false
}
}
}
}
]
少なくとも以下がわかる
- JSDoc によるドキュメンテーション
- コンポーネント名 (≒変数名)
- Props 定義
- JSDoc によるドキュメンテーション
- 型情報
- required/optional
- デフォルト値
JSDoc 解釈したりデフォ値まで見れるのは優秀。
もうちょっと小難しいコンポーネントで試してみる。
例えば Component Driven User Interface から拝借したこのコンポーネントを入力する。
output
[
{
"description": "",
"displayName": "NavGroup",
"methods": [],
"props": {
"links": {
"required": true,
"tsType": {
"name": "Array",
"elements": [
{
"name": "signature",
"type": "object",
"raw": "{ title: string; href: string }",
"signature": {
"properties": [
{
"key": "title",
"value": {
"name": "string",
"required": true
}
},
{
"key": "href",
"value": {
"name": "string",
"required": true
}
}
]
}
}
],
"raw": "link[]"
},
"description": ""
}
}
},
{
"description": "",
"displayName": "Navbar",
"methods": [],
"props": {
"links": {
"required": true,
"tsType": {
"name": "Array",
"elements": [
{
"name": "signature",
"type": "object",
"raw": "{ title: string; href: string }",
"signature": {
"properties": [
{
"key": "title",
"value": {
"name": "string",
"required": true
}
},
{
"key": "href",
"value": {
"name": "string",
"required": true
}
}
]
}
}
],
"raw": "link[]"
},
"description": ""
},
"githubLink": {
"required": true,
"tsType": {
"name": "signature",
"type": "object",
"raw": "{ namespace: string; repo: string }",
"signature": {
"properties": [
{
"key": "namespace",
"value": {
"name": "string",
"required": true
}
},
{
"key": "repo",
"value": {
"name": "string",
"required": true
}
}
]
}
},
"description": ""
}
}
}
]
追加で確認できたこと
- ファイル内に複数コンポーネントがある場合にそれぞれが抽出される
- 配列型の場合はその中の要素についても取り出せている
- 型変数への代入などがあっても、最終的な型情報が取り出される
型の抜き出しについてはどこまで出来るのか気になる。
以下ぐらい対応できれば最低限使えそう。
input
type Props = {
riteral: 'foo' | 'bar' | 'baz'
object: {
foo: string
bar: number
}
array: Array<number | string>
function: (foo: string) => number
}
export const MyComponent = (props: Props) => {
return <div />
}
output
[
{
"description": "",
"displayName": "MyComponent",
"methods": [],
"props": {
"riteral": {
"required": true,
"tsType": {
"name": "union",
"raw": "'foo' | 'bar' | 'baz'",
"elements": [
{
"name": "literal",
"value": "'foo'"
},
{
"name": "literal",
"value": "'bar'"
},
{
"name": "literal",
"value": "'baz'"
}
]
},
"description": ""
},
"object": {
"required": true,
"tsType": {
"name": "signature",
"type": "object",
"raw": "{\n foo: string\n bar: number\n}",
"signature": {
"properties": [
{
"key": "foo",
"value": {
"name": "string",
"required": true
}
},
{
"key": "bar",
"value": {
"name": "number",
"required": true
}
}
]
}
},
"description": ""
},
"array": {
"required": true,
"tsType": {
"name": "Array",
"elements": [
{
"name": "union",
"raw": "number | string",
"elements": [
{
"name": "number"
},
{
"name": "string"
}
]
}
],
"raw": "Array<number | string>"
},
"description": ""
},
"function": {
"required": true,
"tsType": {
"name": "signature",
"type": "function",
"raw": "(foo: string) => number",
"signature": {
"arguments": [
{
"type": {
"name": "string"
},
"name": "foo"
}
],
"return": {
"name": "number"
}
}
},
"description": ""
}
}
}
]
全然大丈夫そう。情報の欠落もここまでは無い。
公式ドキュメント > About react-docgen
- オリジナルの開発は 2015 年の Facebook によるもの
- 現在は別の人に引き継がれて開発が継続され、ライセンスも MIT に変わった
- Babel はじめ、様々な OSS が使用されている
まぁ現在でも Babel ベースなんだな。これを Rust 製のなんらかのパーサーに差し替わったらさらに高速化するとかもあるのかね。
公式ドキュメント > Users
react-docgen
を使用しているサービス。 Storybook
がやっぱりわかりやすい。
CLI を使ってみる
react-docgen
は CLI とライブラリ両方あるので、まず手元のプロジェクトで気軽に試せるように CLI から使ってみる。
@react-docgen/cli
ヘルプ確認
$ yarn react-docgen --help
yarn run v1.22.19
Usage: react-docgen-parse [options] <globs...>
Extract meta information from React components.
Either specify a paths to files or a glob pattern that matches multiple files.
Arguments:
globs Can be globs or paths to files
Options:
-o, --out <file> Store extracted information in the specified file instead of printing to stdout. If the file exists it will be overwritten.
-i, --ignore <glob> Comma separated list of glob patterns which will ignore the paths that match. Can also be used multiple times. (default: ["**/node_modules/**","**/__tests__/**","**/__mocks__/**"])
--no-default-ignores Do not ignore the node_modules, __tests__, and __mocks__ directories.
--pretty Print the output JSON pretty (default: false)
--failOnWarning Fail with exit code 2 on react-docgen component warnings. This includes "no component found" and "multiple components found" warnings. (default: false)
--resolver <resolvers> Built-in resolver config (find-all-components, find-all-exported-components, find-all-annotated-components, find-exported-component), package name or path to a module that exports a resolver. Can also be used multiple times.
When used, no default handlers will be added. (default: ["find-exported-component"])
--importer <importer> Built-in importer name (fsImport, ignoreImporter), package name or path to a module that exports an importer. (default: "fsImporter")
--handler <handlers> Comma separated list of handlers to use. Can also be used multiple times. When used, no default handlers will be added. (default:
["childContextTypeHandler","codeTypeHandler","componentDocblockHandler","componentMethodsHandler","componentMethodsJsDocHandler","contextTypeHandler","defaultPropsHandler","displayNameHandler","propDocblockHandler","propTypeCompositionHandler","propTypeHandler"])
-h, --help display help for command
必須は glob だけっぽいので、とりあえず手元のプロジェクトの全コンポーネントに実行してみる。
$ yarn react-docgen 'client/**/components/**/!(*.stories|*.test).tsx' --pretty -o docgen.json
巨大なJSONファイルが生成された。ファイル名をキーとして、そのファイルから返されるコンポーネントの定義が含まれてる。以外とすんなり動きはした。
気になったこと1
以下みたいなコンポーネントだと情報が取得できない。
type Props = {
type: 'bar' | 'baz'
}
export const SampleComponent: FC<Props> = ({ type }) => {
const components = {
bar: <div />,
baz: <span />,
}
return components[type]
}
ちゃんと分岐書くと大丈夫になる。
type Props = {
type: 'bar' | 'baz'
}
export const SampleComponent: FC<Props> = ({ type }) => {
if (type === 'bar') {
return <div />
} else {
return <span />
}
}
どっちも型上はちゃんと React コンポーネントとして成立してるんだけどな。
いずれにしても、そういう場合は以下のような警告を出してくれるので、完全に見落とすというのは無さそう。
▶ WARNING: No suitable component definition found. 👀
in client/src/components/hogehoge.tsx
気になったこと2
1ファイルが複数コンポーネント返すと警告になる。
export const SampleA = () => <div />
export const SampleB = () => <div />
Playground だと大丈夫だったんだけどな。原則1ファイル1コンポーネントしか export しなければ問題ないだろうけど。
▶ WARNING: Multiple exported component definitions found. 👀
気になったこと3
以下のように同ファイル内にある型変数を参照している場合
type User = {
id: string
name: string
}
type Props = {
users: User[]
}
export const UserList: FC<Props> = ({ users }) => (
<li>
{users.map((user) => (
<span key={user.id}>{user.name}</span>
))}
</li>
)
Props は適切に展開される
{
"description": "",
"methods": [],
"displayName": "UserList",
"props": {
"users": {
"required": true,
"tsType": {
"name": "Array",
"elements": [
{
"name": "signature",
"type": "object",
"raw": "{\n id: string\n name: string\n}",
"signature": {
"properties": [
{
"key": "id",
"value": {
"name": "string",
"required": true
}
},
{
"key": "name",
"value": {
"name": "string",
"required": true
}
}
]
}
}
],
"raw": "User[]"
},
"description": ""
}
}
}
Lookup型とか使いだしちゃうと
type User = {
id: string
name: string
}
type Props = {
userIds: Array<User['id']>
}
export const UserList: FC<Props> = ({ userIds }) => (
<li>
{userIds.map((userId, index) => (
<ul>{userId}</ul>
))}
</li>
)
展開されない。
{
"description": "",
"methods": [],
"displayName": "UserList",
"props": {
"userIds": {
"required": true,
"tsType": {
"name": "Array",
"elements": [
{
"name": "string",
"raw": "User['id']"
}
],
"raw": "Array<User['id']>"
},
"description": ""
}
}
}
Lookup 型が悪いのか、他にも限界があるのかはわからない。
CLI の用途例
とりあえず1ファイル1コンポーネントを export しているという大前提であれば、コンポーネントの一覧はこれだけで取得できそう。
cat docgen.json | jq 'keys'
総数だけならこう
$ cat docgen.json | jq 'keys | length'
あー、ここまで書いて、Storybook の対応が必要なコンポーネントを洗い出す用途で使えるかもしれんなぁと思った。
例えばこんなスクリプトを書いて
const { readFile } = require('fs/promises')
;(async () => {
const docgen = JSON.parse(await readFile('./docgen.json', 'utf-8'))
const fileNames = Object.keys(docgen)
fileNames.forEach((fileName) => {
const doc = docgen[fileName][0]
const displayName = doc.displayName
const propsCount = doc.props ? Object.keys(doc.props).length : 0
console.log(`${propsCount} ${fileName}`)
})
})()
実行すると、コンポーネントごとの Props 数を一覧できる。 Props が多いコンポーネントのほうが描画パターンが多いから優先的にやるべきとかの判断ができるやも?