diff --git a/package.json b/package.json index 5df08ef1..64f36956 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "react-docgen-typescript", - "version": "0.0.4", + "version": "0.0.5", "description": "", "main": "lib/index.js", "scripts": { "prepublish": "tsc -d", "test": "tsc && mocha ./lib/**/__tests__/**.js", + "test:debug": "tsc && mocha --debug ./lib/**/__tests__/**.js", "example": "tsc && node ./node_modules/react-styleguidist/bin/styleguidist server --config ./examples/react-styleguidist-example/styleguide.config.js" }, "author": "pvasek", diff --git a/src/__tests__/data/ColumnWithoutExportedProps.tsx b/src/__tests__/data/ColumnWithoutExportedProps.tsx new file mode 100644 index 00000000..69347096 --- /dev/null +++ b/src/__tests__/data/ColumnWithoutExportedProps.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +/** + * Column properties. + */ +interface IColumnProps extends React.HTMLAttributes { + /** prop1 description */ + prop1?: string; + /** prop2 description */ + prop2: number; + /** + * prop3 description + */ + prop3: () => void; + /** prop4 description */ + prop4: 'option1' | 'option2' | "option3"; +} + +/** + * Form column. + */ +export class Column extends React.Component { + public static defaultProps: Partial = { + prop1: 'prop1' + }; + + render() { + const {prop1} = this.props; + return
{prop1}
; + } +} + +export default Column; \ No newline at end of file diff --git a/src/__tests__/docgenConverter.spec.ts b/src/__tests__/docgenConverter.spec.ts new file mode 100644 index 00000000..e4cc568f --- /dev/null +++ b/src/__tests__/docgenConverter.spec.ts @@ -0,0 +1,102 @@ +import { assert } from 'chai'; +import * as path from 'path'; +import { getDocumentation } from '../parser'; +import { convertToDocgen, StyleguidistComponent } from "../docgenConverter"; + +describe('docgenConverter', () => { + it('Should work with class Component', () => { + const result = convertToDocgen({ + classes: [ + { + name: 'name1', + comment: 'comment1', + extends: 'Component', + propInterface: 'PropsInterface', + } + ], + interfaces: [ + { + name: 'PropsInterface', + comment: 'props comment', + members: [ + { + name: 'prop1', + comment: 'prop1 comment', + isRequired: true, + text: 'prop1 text', + type: 'prop1 type' + } + ] + } + ] + }); + + assert.equal('name1', result.displayName); + assert.equal('comment1', result.description); + const prop1Result = result.props['prop1']; + assert.equal('prop1 type', prop1Result.type.name); + assert.equal('prop1 comment', prop1Result.description); + assert.equal(true, prop1Result.required); + }); + + it('Should work with functional StatelessComponent', () => { + const result = convertToDocgen({ + classes: [ + { + name: 'name1', + comment: 'comment1', + extends: 'StatelessComponent', + propInterface: 'PropsInterface', + } + ], + interfaces: [ + { + name: 'PropsInterface', + comment: 'props comment', + members: [ + { + name: 'prop1', + comment: 'prop1 comment', + isRequired: true, + text: 'prop1 text', + type: 'prop1 type' + } + ] + } + ] + }); + + assert.equal('name1', result.displayName); + assert.equal('comment1', result.description); + const prop1Result = result.props['prop1']; + assert.equal('prop1 type', prop1Result.type.name); + assert.equal('prop1 comment', prop1Result.description); + assert.equal(true, prop1Result.required); + }); + + it('Should work without props interface', () => { + let result: StyleguidistComponent = null; + const originalWarn = console.warn; + let warnCallCount = 0; + console.warn = () => warnCallCount++; + try { + result = convertToDocgen({ + classes: [ + { + name: 'name1', + comment: 'comment1', + extends: 'Component', + propInterface: null, + } + ], + interfaces: [] + }); + } finally { + console.warn = originalWarn; + } + + assert.equal(1, warnCallCount); + assert.equal('name1', result.displayName); + assert.equal('comment1', result.description); + }); +}); \ No newline at end of file diff --git a/src/__tests__/parser.spec.ts b/src/__tests__/parser.spec.ts index 87461544..953f69e5 100644 --- a/src/__tests__/parser.spec.ts +++ b/src/__tests__/parser.spec.ts @@ -37,6 +37,20 @@ describe('parser', () => { assert.equal(true, i.members[3].isRequired); }); + it('Should parse class-based components with unexported props interface', () => { + const fileName = path.join(__dirname, '../../src/__tests__/data/ColumnWithoutExportedProps.tsx'); // it's running in ./temp + const result = getDocumentation(fileName); + assert.ok(result.classes); + assert.ok(result.interfaces); + assert.equal(1, result.classes.length); + assert.equal(0, result.interfaces.length); + + const c = result.classes[0]; + assert.equal('Column', c.name); + assert.equal('Form column.', c.comment); + assert.equal('Component', c.extends); + }); + it('Should parse functional components', () => { const fileName = path.join(__dirname, '../../src/__tests__/data/Row.tsx'); // it's running in ./temp const result = getDocumentation(fileName); diff --git a/src/docgenConverter.ts b/src/docgenConverter.ts index b56f74ed..60a03088 100644 --- a/src/docgenConverter.ts +++ b/src/docgenConverter.ts @@ -1,6 +1,16 @@ -import { FileDoc } from './parser'; +import { FileDoc, InterfaceDoc, MemberDoc } from './parser'; -export function convertToDocgen(doc: FileDoc) { +export interface StyleguidistProps { + [key: string]: PropItem; +} + +export interface StyleguidistComponent { + displayName: string; + description: string; + props: StyleguidistProps; +} + +export function convertToDocgen(doc: FileDoc): StyleguidistComponent { const reactClasses = doc.classes.filter(i => i.extends === 'Component' || i.extends === 'StatelessComponent'); if (reactClasses.length === 0) { @@ -8,28 +18,18 @@ export function convertToDocgen(doc: FileDoc) { } const comp = reactClasses[0]; const reactInterfaces = doc.interfaces.filter(i => i.name === comp.propInterface); - if (reactInterfaces.length === 0) { - return null; + + let props: any = {}; + if (reactInterfaces.length !== 0) { + props = getProps(reactInterfaces[0]); + } else { + console.warn('REACT-DOCGEN-TYPESCRIPT It seems that your props type is not exported. Add \'export\' keyword to your props definition.'); } - const props = reactInterfaces[0]; return { displayName: comp.name, description: comp.comment, - props: props.members.reduce((acc, i) => { - const item: PropItem = { - description: i.comment, - type: {name: i.type}, - defaultValue: null, - required: i.isRequired - }; - if (i.values) { - item.description = item.description + ' (one of the following:' + i.values.join(',') + ')'; - } - - acc[i.name] = item; - return acc; - }, {}) + props: props } } @@ -54,6 +54,22 @@ export interface Docgen { props: PropsObject; } +function getProps(props: InterfaceDoc): StyleguidistProps { + return props.members.reduce((acc, i) => { + const item: PropItem = { + description: i.comment, + type: {name: i.type}, + defaultValue: null, + required: i.isRequired + }; + if (i.values) { + item.description = item.description + ' (one of the following:' + i.values.join(',') + ')'; + } + + acc[i.name] = item; + return acc; + }, {}); +} /* { "props": { diff --git a/src/index.ts b/src/index.ts index c5f5103b..5a916887 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,8 @@ import { parse } from './propTypesParser'; +import { StyleguidistComponent, StyleguidistProps } from './docgenConverter'; export { parse, + StyleguidistComponent, + StyleguidistProps, } \ No newline at end of file diff --git a/src/parser.ts b/src/parser.ts index 246419be..927d0a20 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -66,7 +66,7 @@ export function getDocumentation(fileName: string, options: ts.CompilerOptions = }; classes.push(classObj); } - if (node.kind === ts.SyntaxKind.ClassDeclaration) { + if (node.kind === ts.SyntaxKind.ClassDeclaration) { const classNode = node as ts.ClassDeclaration; const symbol = checker.getSymbolAtLocation(classNode.name); @@ -78,11 +78,14 @@ export function getDocumentation(fileName: string, options: ts.CompilerOptions = .filter(i => i.kind === ts.SyntaxKind.Identifier) .map((i: ts.Identifier) => i.text); + const componentIndex = list.indexOf('Component'); + const propsIndex = componentIndex === -1 ? -1 : (componentIndex + 1); + classes.push({ name: symbol.name, comment: ts.displayPartsToString(symbol.getDocumentationComment()), - extends: list.length > 0 && list.indexOf('Component') > -1 ? 'Component' : null, - propInterface: list.length > 1 ? list[1] : null, + extends: list.length > 0 && componentIndex > -1 ? 'Component' : null, + propInterface: list.length > propsIndex ? list[propsIndex] : null, }); } diff --git a/src/propTypesParser.ts b/src/propTypesParser.ts index f8657fa9..59b9dda0 100644 --- a/src/propTypesParser.ts +++ b/src/propTypesParser.ts @@ -1,10 +1,10 @@ import { getDocumentation } from './parser'; -import { convertToDocgen } from './docgenConverter'; +import { convertToDocgen, StyleguidistComponent } from './docgenConverter'; /** * Parser given file and return documentation in format compatibe with react-docgen. */ -export function parse(filePath: string) { +export function parse(filePath: string): StyleguidistComponent { const doc = getDocumentation(filePath); return convertToDocgen(doc); } \ No newline at end of file