Skip to content
This repository has been archived by the owner on Jan 5, 2023. It is now read-only.

Commit

Permalink
feat: support for react-docgen-typescript-loader
Browse files Browse the repository at this point in the history
  • Loading branch information
nekitk committed Aug 13, 2019
1 parent 6303f37 commit b5586cb
Show file tree
Hide file tree
Showing 14 changed files with 304 additions and 89 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules/
dist
settings.json
.idea
25 changes: 16 additions & 9 deletions .storybook/webpack.config.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
const path = require('path')

module.exports = {
module: {
rules: [
module.exports = async ({ config }) => {
config.module.rules.push({
test: /\.(ts|tsx)$/,
use: [
'react-docgen-typescript-loader',
{
test: /\.(css)$/,
loaders: ['style-loader', 'css-loader'],
include: path.resolve(__dirname, '../')
loader: 'ts-loader',
options: {
transpileOnly: true,
},
}
]
}
}
],
});

config.resolve.extensions.push('.ts', '.tsx');

return config;
};
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Smart knobs addon for Storybook

This Storybook plugin uses `@storybook/addon-knobs` but creates the knobs automatically based on PropTypes and Flow.
This Storybook plugin uses `@storybook/addon-knobs` but creates the knobs automatically based on PropTypes, Flow and Typescript.

## Installation:

Expand All @@ -11,7 +11,8 @@ npm i storybook-addon-smart-knobs --save-dev
## Usage:

```js
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { storiesOf } from '@storybook/react'
import { withKnobs } from '@storybook/addon-knobs'
import { withSmartKnobs } from 'storybook-addon-smart-knobs'
Expand Down Expand Up @@ -53,3 +54,7 @@ module.exports = (baseConfig, env, defaultConfig) => {
return defaultConfig
}
```

## Typescript:

Use [react-docgen-typescript-loader](https://github.com/strothj/react-docgen-typescript-loader) to generate docgen info from Typescript types. This docgen info will be used to automatically create knobs.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.SmartKnobedComponent {
.PropTable {
border: 1px solid #999;
color: #333;
font-family: monospace;
Expand All @@ -7,17 +7,17 @@
text-align: left;
}

.SmartKnobedComponent thead {
.PropTable thead {
font-style: italic;
font-size: 0.85em;
}

.SmartKnobedComponent th {
.PropTable th {
background-color: #eee;
}

.SmartKnobedComponent th,
.SmartKnobedComponent td {
.PropTable th,
.PropTable td {
border: 0.05em solid #999;
min-width: 1em;
padding: 0.4em;
Expand Down
32 changes: 32 additions & 0 deletions example/stories/PropTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react'
import PropTypes from 'prop-types'

import './PropTable.css'

export const PropTable = ({ docgenInfo, ...props }) => (
<table className='PropTable'>
<thead>
<tr>
<th>Property</th>
<th>PropType</th>
<th>Value</th>
<th>typeof</th>
</tr>
</thead>
<tbody>{
Object.keys(props)
.map(prop => (
<tr key={ prop }>
<th>{ prop }</th>
<td>{ (docgenInfo.props[prop].type || docgenInfo.props[prop].flowType || {}).name }</td>
<td>{ typeof props[prop] === 'function' ? <i>function</i> : JSON.stringify(props[prop]) || '(empty)' }</td>
<td>{ typeof props[prop] }</td>
</tr>
))
}</tbody>
</table>
)

PropTable.propTypes = {
docgenInfo: PropTypes.object.isRequired
}
25 changes: 2 additions & 23 deletions example/stories/SmartKnobedComponent.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,9 @@
import React from 'react'
import PropTypes from 'prop-types'

import './SmartKnobedComponent.css'
import { PropTable } from './PropTable'

const SmartKnobedComponent = props => (
<table className='SmartKnobedComponent'>
<thead>
<tr>
<th>Property</th>
<th>PropType</th>
<th>Value</th>
<th>typeof</th>
</tr>
</thead>
<tbody>
{Object.keys(props).map(prop => (
<tr key={ prop }>
<th>{ prop }</th>
<td>{ SmartKnobedComponent.__docgenInfo.props[prop].type.name }</td>
<td>{ typeof props[prop] === 'function' ? <i>function</i> : JSON.stringify(props[prop]) || '(empty)' }</td>
<td>{ typeof props[prop] }</td>
</tr>
))}
</tbody>
</table>
)
const SmartKnobedComponent = props => <PropTable { ...props } docgenInfo={ SmartKnobedComponent.__docgenInfo } />

SmartKnobedComponent.propTypes = {
bool: PropTypes.bool,
Expand Down
5 changes: 3 additions & 2 deletions example/stories/SmartKnobedComponentMissingProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
import React from 'react'
import PropTypes from 'prop-types'

import { PropTable } from './PropTable'

const SmartKnobedComponentMissingProps = ({
foo = '',
bar = 'bar',
}) => (
<code>
<p>You should see a console.warn about a prop with default value bar.</p>
<p>{foo}</p>
<p>{bar}</p>
<PropTable foo={ foo } bar={ bar } docgenInfo={ SmartKnobedComponentMissingProps.__docgenInfo } />
</code>
)

Expand Down
26 changes: 3 additions & 23 deletions example/stories/SmartKnobedComponentWithFlow.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow
import React from 'react'
import './SmartKnobedComponent.css'

import { PropTable } from './PropTable'

/* eslint-disable */
type PropType = {
Expand All @@ -13,27 +14,6 @@ type PropType = {
}
/* eslint-enable */

const SmartKnobedComponent = (props: PropType) => (
<table className='SmartKnobedComponent'>
<thead>
<tr>
<th>Property</th>
<th>PropType</th>
<th>Value</th>
<th>typeof</th>
</tr>
</thead>
<tbody>
{Object.keys(props).map(prop => (
<tr key={ prop }>
<th>{ prop }</th>
<td>{ SmartKnobedComponent.__docgenInfo.props[prop].flowType.name }</td>
<td>{ typeof props[prop] === 'function' ? <i>function</i> : JSON.stringify(props[prop]) || '(empty)' }</td>
<td>{ typeof props[prop] }</td>
</tr>
))}
</tbody>
</table>
)
const SmartKnobedComponent = (props: PropType) => <PropTable { ...props } docgenInfo={ SmartKnobedComponent.__docgenInfo } />

export default SmartKnobedComponent
16 changes: 16 additions & 0 deletions example/stories/SmartKnobedComponentWithTypescript.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react'

import {PropTable} from './PropTable'

interface IProps {
bool: boolean;
number: number;
string: string;
func: () => void;
object: {};
node: React.ReactNode;
oneOf?: 'one' | 'two' | 'three';
}

export const SmartKnobedComponentWithTypescript: React.FC<IProps> & { __docgenInfo?: any } = (props) =>
<PropTable {...props} docgenInfo={SmartKnobedComponentWithTypescript.__docgenInfo} />
2 changes: 2 additions & 0 deletions example/stories/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { withKnobs, select } from '@storybook/addon-knobs'
import SmartKnobedComponent from './SmartKnobedComponent'
import SmartKnobedComponentMissingProps from './SmartKnobedComponentMissingProps'
import SmartKnobedComponentWithFlow from './SmartKnobedComponentWithFlow'
import { SmartKnobedComponentWithTypescript } from './SmartKnobedComponentWithTypescript'

const stub = fn => fn()

Expand All @@ -14,6 +15,7 @@ storiesOf('Basic', module)
.addDecorator(withKnobs)
.add('proptypes', () => <SmartKnobedComponent />)
.add('flow', () => <SmartKnobedComponentWithFlow />)
.add('typescript', () => <SmartKnobedComponentWithTypescript />)

storiesOf('Missing props', module)
.addDecorator(withSmartKnobs)
Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,20 @@
"@storybook/addon-options": "^5.0.0",
"@storybook/client-logger": "^5.0.0",
"@storybook/react": "^5.0.0",
"@types/react": "^16.9.1",
"babel-loader": "^8.0.4",
"css-loader": "^3.1.0",
"core-js": "^3.0.1",
"css-loader": "^3.1.0",
"eslint-config-taller": "^2.0.0",
"eslint-plugin-flowtype": "^3.5.1",
"jest": "^24.7.1",
"prop-types": "^15.7.2",
"react": "^16.8.6",
"react-docgen-typescript-loader": "^3.1.1",
"react-dom": "^16.8.6",
"rimraf": "^2.6.3"
"rimraf": "^2.6.3",
"ts-loader": "^6.0.4",
"typescript": "^3.5.3"
},
"peerDependencies": {
"@storybook/addon-knobs": "^5.0.0",
Expand Down
55 changes: 37 additions & 18 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { action } from '@storybook/addon-actions'
import { logger } from '@storybook/client-logger'
import { text, boolean, number, object, select } from '@storybook/addon-knobs'

const QUOTED_STRING_REGEXP = /^['"](.*)['"]$/
const cleanupString = str => str.replace(QUOTED_STRING_REGEXP, '$1')

const knobResolvers = {}
export const addKnobResolver = ({ name, resolver, weight = 0 }) => (knobResolvers[name] = { name, resolver, weight })

Expand All @@ -11,9 +14,11 @@ export const addKnobResolver = ({ name, resolver, weight = 0 }) => (knobResolver
* --------------------------------
*/

export const propTypeKnobResolver = (name, knob, ...args) =>
(propName, propType, value, propTypes, defaultProps) =>
propType.type.name === name ? knob(propName, value, ...args) : undefined
export const propTypeKnobResolver = (name, regexp, knob, ...args) =>
(propName, propType, value) =>
(propType.type.name === name || (regexp && regexp.test(propType.type.name)))
? knob(propName, value, ...args)
: undefined

const flowTypeKnobsMap = [
{ name: 'signature', knob: (name, value) => value || action(name) },
Expand All @@ -25,45 +30,54 @@ const flowTypeKnobsMap = [
const propTypeKnobsMap = [
{ name: 'string', knob: text },
{ name: 'number', knob: number },
{ name: 'bool', knob: boolean },
{ name: 'func', knob: (name, value) => value || action(name) },
{ name: 'object', knob: object },
{ name: 'node', knob: text },
{ name: 'bool', knob: boolean },
{ name: 'func', regexp: /=>/, knob: (name, value) => value || action(name) },
{ name: 'object', regexp: /^{.*}$/, knob: object },
{ name: 'node', regexp: /^ReactNode$/, knob: text },
{ name: 'element', knob: text },
{ name: 'array', knob: object },
{ name: 'array', regexp: /\[]$|^\[.*]$/, knob: object },
]

const typeKnobsMap = [...flowTypeKnobsMap, ...propTypeKnobsMap]

typeKnobsMap.forEach(({ name, knob, args = [] }, weight) => addKnobResolver({
typeKnobsMap.forEach(({ name, regexp, knob, args = [] }, weight) => addKnobResolver({
weight: weight * 10,
name: `PropTypes.${name}`,
resolver: propTypeKnobResolver(name, knob, ...args)
resolver: propTypeKnobResolver(name, regexp, knob, ...args)
}))

const optionsReducer = (res, value) => ({ ...res, [value]: value })
const withDefaultOption = (options) => ({ '': '--', ...options })
const createSelect = (propName, elements, defaultProps) => {
const createSelect = (propName, elements, defaultValue) => {
try {
const options = elements
// Cleanup string quotes, if any.
.map(value => value.value.replace(/^['"](.*)['"]$/, '$1'))
.map(value => cleanupString(value.value))
.reduce(optionsReducer, {})
return select(propName, withDefaultOption(options), defaultProps[propName])

return select(propName, withDefaultOption(options), defaultValue)
}
catch (e) { }
}

// Register 'oneOf' PropType knob resolver.
addKnobResolver({
name: 'PropTypes.oneOf',
resolver: (propName, propType, value, propTypes, defaultProps) => {
resolver: (propName, propType, defaultValue) => {
if (propType.type.name === 'enum' && propType.type.value.length) {
return createSelect(propName, propType.type.value, defaultProps)
return createSelect(propName, propType.type.value, defaultValue)
}
// for flow support
if (propType.type.name === 'union' && propType.type.elements) {
return createSelect(propName, propType.type.elements, defaultProps)
return createSelect(propName, propType.type.elements, defaultValue)
}
// for typescript support
if (propType.type.name.includes('|')) {
const values = propType.type.name.split('|').map(v => v.trim())

if (values.length && values.every(value => QUOTED_STRING_REGEXP.test(value))) {
return createSelect(propName, values.map(value => ({ value })), defaultValue)
}
}
}
})
Expand Down Expand Up @@ -114,8 +128,13 @@ const resolvePropValues = (propTypes, defaultProps) => {

return propNames
.map(propName => resolvers.reduce(
(value, resolver) => value !== undefined ? value
: resolver(propName, propTypes[propName], defaultProps[propName], propTypes, defaultProps),
(value, resolver) => {
const propType = propTypes[propName] || {}
const defaultValue = (propType.defaultValue && cleanupString(propType.defaultValue.value || '')) || undefined

return value !== undefined ? value
: resolver(propName, propType, defaultValue)
},
undefined
))
.reduce((props, value, i) => ({
Expand Down
6 changes: 6 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"jsx": "react",
"module": "esnext"
}
}
Loading

0 comments on commit b5586cb

Please sign in to comment.