やりたいこと
storybook のカタログを flow から自動生成できないか、と思って調べていた。
こんなコードがあったとする。
index.js
/* @flow */
type Point = {
x: number,
y: number
}
type Props = {
a: string,
p: Point,
c: {
d: { e: any }
},
arr: Array<number>
}
const props: Props = {
a: 'foo',
p: { x: 1, y: 2 },
c: { d: { e: 3 } },
arr: [1]
}
export default props
これから props のインスタンスを知らない状態で、型からそれを満たすインスタンスを生成したい。
flow-bin と flow-parser-bin を使って export default されてるオブジェクトの型の推論結果を取得し、そこからインスタンスを再生成する。
/* @flow */
const fs = require('fs')
const { execFileSync } = require('child_process')
const flow = require('flow-bin')
const parser = require('flow-parser-bin')
const debug = obj => console.log(JSON.stringify(obj, null, 2))
function getTypeAtPos(fpath: string, line: number, column: number) {
const ret = execFileSync(flow, [
'type-at-pos',
fpath,
`${line}`,
`${column}`,
'--json'
]).toString()
return JSON.parse(ret)
}
function getExportDefaultType(fpath: string): string | null {
const body = fs.readFileSync(fpath).toString()
const ast = parser.parse(body)
const exportDefaultNode = ast.body.find(
node => node.type === 'ExportDefaultDeclaration'
)
if (exportDefaultNode) {
const start = exportDefaultNode.declaration.loc.start
const { type } = getTypeAtPos(fpath, start.line, start.column + 1)
return type
}
return null
}
function astToObject(node) {
switch (node.type) {
case 'ObjectTypeAnnotation': {
return node.properties.reduce((acc, property) => {
// TODO: Check key is optional
return { ...acc, [property.key.name]: astToObject(property.value) }
}, {})
}
case 'GenericTypeAnnotation': {
if (node.id.name === 'Array') {
return [astToObject(node.typeParameters.params[0])]
} else {
// TODO: Trace type instance
return {}
}
}
case 'NumberTypeAnnotation': {
return 42
}
case 'StringTypeAnnotation': {
return '<string>'
}
case 'AnyTypeAnnotation': {
return '<any>'
}
default: {
console.log('missing:', node.type)
return null
}
}
}
const typeExprString = getExportDefaultType('index.js')
if (typeExprString) {
const typeExpr = parser.parse('type T = ' + typeExprString).body[0].right
debug(astToObject(typeExpr))
}
実行するとこうなる
{
"a": "<string>",
"arr": [
42
],
"c": {
"d": {
"e": "<any>"
}
},
"p": {
"x": 42,
"y": 42
}
}
課題
type-at-pos では Generics の参照を解決しきれていない。
このとき
type Optional<T> = T | null
const t: Optional<string> = null
export default t
AST は Optional 定義を探しにいかないとならない
{
"type": "GenericTypeAnnotation",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 25
}
},
"range": [
9,
25
],
"id": {
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 17
}
},
"range": [
9,
17
],
"name": "Optional",
"typeAnnotation": null,
"optional": false
},
"typeParameters": {
"type": "TypeParameterInstantiation",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 17
},
"end": {
"line": 1,
"column": 25
}
},
"range": [
17,
25
],
"params": [
{
"type": "StringTypeAnnotation",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 18
},
"end": {
"line": 1,
"column": 24
}
},
"range": [
18,
24
]
}
]
}
}
まだやり方がわからないので、わかったら書く。