LoginSignup
6

More than 5 years have passed since last update.

Flow で型が書かれた React.Component から storybook を自動生成してみた

Last updated at Posted at 2018-02-22

作ったけど全然未完成です。いろんな都合で黒魔術です。

まだ全然。どっちかというとASTで遊んでみた系ですが、これを想定してない趣味プロジェクトのcomponentsを対象に突っ込んだら、意外と半分ぐらいは動きました。

なにこれ

flow が入ってることは前提

$ yarn add -D flow-parser-bin flow-bin flowed-story

こんなコードがあったとして

src/components/Foo.js
/* @flow */
import React from 'react'
export default (props: { x: string }) => <div>Foo: {props.x}</div>
$ npm run flowed-story 'src/components/*.js'

export default するインスタンスに型を書いてたら, 型からpropsを自動生成してストーリーを作成します。

どうやって実装してるか

こういうコードを中で生成してます

/* @flow */
import React from 'react'
import { storiesOf } from '@storybook/react'
const ctx = storiesOf('Generated by flow', module)
import C from '../../src/components/Foo.js'
ctx.add('../../src/components/Foo.js', () => <C {...{"x":"<string: props.x>"}}/>)

flow type-at-pos は指定のファイル上の指定された座標のシンボルの型が取れるんですが、意外とうまいこと抽出させてくれません。 language-server の API はまだ unstable で公開されてません。

なので、パースしやすいように、指定されたファイルをターゲットに次のコードを生成します。

/* @flow */
import _0_App from '../../src/components/App.js'
import _1_Foo from '../../src/components/Foo.js'
import _2_Generics from '../../src/components/Generics.js'
import _3_Nested from '../../src/components/nested/Nested.js'
/* SYMBOLS */
_0_App //: {"line": 7, "symbolFor": "../../src/components/App.js"}
_1_Foo //: {"line": 8, "symbolFor": "../../src/components/Foo.js"}
_2_Generics //: {"line": 9, "symbolFor": "../../src/components/Generics.js"}
_3_Nested //: {"line": 10, "symbolFor": "../../src/components/nested/Nested.js"}

こんなコードで型を取り出します。


/* @flow */
const flow = require('flow-bin')
const { execFileSync } = require('child_process')

export function getTypeAtPos(fpath: string, line: number, column: number) {
  const ret = execFileSync(flow, [
    'type-at-pos',
    fpath,
    `${line}`,
    `${column}`,
    '--json'
  ]).toString()
  return JSON.parse(ret)
}

ガチャガチャやって 型の表現から 型の AST を取ったら、AST からモック用の props を生成します。

/* @flow */
export default function astToObject(node, nodePath = 'props') {
  switch (node.type) {
    case 'ObjectTypeAnnotation': {
      return node.properties.reduce((acc, property) => {
        // TODO: Check key is optional
        const nextPath = nodePath + '.' + property.key.name
        return {
          ...acc,
          [property.key.name]: astToObject(property.value, nextPath)
        }
      }, {})
    }
    case 'GenericTypeAnnotation': {
      if (node.id.name === 'Array') {
        const nextPath = nodePath + '.*'
        return [astToObject(node.typeParameters.params[0], nextPath)]
      } else {
        // TODO: Trace type instance
        return null
      }
    }
    case 'NumberTypeAnnotation': {
      return 42
    }
    case 'StringTypeAnnotation': {
      return `<string: ${nodePath}>`
    }
    case 'AnyTypeAnnotation': {
      return `<any: ${nodePath}>`
    }
    default: {
      console.error('missing:', node.type)
      return null
    }
  }
}

本当は Quickcheck っぽくしたかったんですが、POCとしては一旦はこれでいいやと諦めました。

なぜ作ったか

みな storybook のメンテで疲弊していますね? だったら型から自動生成できないか? という話です。storyを書くより、好き勝手コードを書けば良いんです。

検証しながら作ったのでコードがまだぐちゃぐちゃなんですが、もっとよく型を取れる方法があるとか、こうしたら現場で使いたいとか、コードが気に喰わないとかあればPRよろしくお願いします。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6