├── .tool-versions ├── .labrc.js ├── .npmignore ├── .lintstagedrc ├── .prettierrc ├── .github └── workflows │ └── npmpublish.yml ├── .gitignore ├── LICENSE ├── package.json ├── test ├── plugin-render-tests.js ├── utils.js ├── header-render-tests.js ├── composes-render-tests.js ├── prop-metadata-render-tests.js ├── complex-props-render-tests.js └── simple-render-tests.js ├── lib ├── defaultTemplate.js └── index.js └── README.md /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 10.15.0 2 | -------------------------------------------------------------------------------- /.labrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | coverage: true, 3 | threshold: 100, 4 | globals: '__core-js_shared__', 5 | shuffle: true 6 | }; -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Test files 2 | test 3 | .labrc.js 4 | coverage.html 5 | 6 | # version manager files 7 | .tool-versions 8 | .nvmrc 9 | 10 | 11 | .lintstagedrc 12 | .prettierrc -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "linters": { 3 | "*.js": ["./node_modules/.bin/prettier --write", "git add"], 4 | "*.{md,json}": ["./node_modules/.bin/prettier --write", "git add"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "fluid": false 11 | } -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 12 15 | - run: npm ci 16 | - run: npm test 17 | 18 | publish-npm: 19 | needs: build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v1 23 | - uses: actions/setup-node@v1 24 | with: 25 | node-version: 12 26 | registry-url: https://registry.npmjs.org/ 27 | - run: npm ci 28 | - run: npm publish 29 | env: 30 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage.html 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Webstorm 40 | .idea 41 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-docgen-markdown-renderer", 3 | "version": "2.1.3", 4 | "description": "Renders a markdown template based on a react-docgen object", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "lab", 8 | "test:debug": "node --inspect-brk=0.0.0.0 ./node_modules/.bin/lab --coverage-exclude lib", 9 | "test:coverage:html": "lab -r console -o stdout -r html -o coverage.html" 10 | }, 11 | "keywords": [ 12 | "react", 13 | "react-docgen", 14 | "component", 15 | "md", 16 | "markdown" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/OriR/react-docgen-markdown-renderer" 21 | }, 22 | "author": "Ori Riner", 23 | "license": "MIT", 24 | "peerDependencies": { 25 | "react-docgen": "^2 || ^3" 26 | }, 27 | "dependencies": { 28 | "react-docgen-renderer-template": "^0.1.0" 29 | }, 30 | "devDependencies": { 31 | "code": "^5.2.0", 32 | "husky": "^1.3.1", 33 | "lab": "^15.5.0", 34 | "lint-staged": "^8.1.4", 35 | "prettier": "^1.16.4", 36 | "react-docgen": "^3.0.0" 37 | }, 38 | "husky": { 39 | "hooks": { 40 | "pre-commit": "lint-staged" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/plugin-render-tests.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('code'); 2 | const Lab = require('lab'); 3 | const lab = (exports.lab = Lab.script()); 4 | const reactDocgen = require('react-docgen'); 5 | const ReactDocGenMarkdownRenderer = require('../lib/index'); 6 | const { template, type } = require('react-docgen-renderer-template'); 7 | const { simpleComponent, simpleMarkdown } = require('./utils'); 8 | 9 | lab.experiment('plugin render', () => { 10 | lab.test('new type', () => { 11 | const docgen = reactDocgen.parse( 12 | simpleComponent({ 13 | componentName: 'MyComponent', 14 | props: [{ name: 'newTypeProp', type: 'MyAwesomeType', custom: true }], 15 | }), 16 | ); 17 | 18 | docgen.props.newTypeProp.type.name = 'awesome'; 19 | 20 | const renderer = new ReactDocGenMarkdownRenderer({ 21 | template: ReactDocGenMarkdownRenderer.defaultTemplate.setPlugins([ 22 | { 23 | getTypeMapping({ extension }) { 24 | return { 25 | awesome: type`${({ context, getType }) => { 26 | return context.type.raw; 27 | }}`, 28 | }; 29 | }, 30 | }, 31 | ]), 32 | }); 33 | 34 | const result = renderer.render('./some/path', docgen, []); 35 | 36 | expect(result).to.equal( 37 | simpleMarkdown({ types: [{ name: 'newTypeProp', value: 'MyAwesomeType' }] }), 38 | ); 39 | }); 40 | 41 | lab.test('custom template', () => { 42 | const docgen = reactDocgen.parse( 43 | simpleComponent({ 44 | componentName: 'MyComponent', 45 | }), 46 | ); 47 | 48 | const renderer = new ReactDocGenMarkdownRenderer({ 49 | template: template({})`${({ context }) => 50 | `This is my awesome template! I can even use the context here! look: ${ 51 | context.componentName 52 | }`}`, 53 | }); 54 | 55 | const result = renderer.render('./some/path', docgen, []); 56 | 57 | expect(result).to.equal( 58 | 'This is my awesome template! I can even use the context here! look: MyComponent', 59 | ); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | 3 | module.exports = { 4 | simpleComponent: ({ 5 | imports = '', 6 | componentName, 7 | props = [], 8 | description = '', 9 | composes = [], 10 | extra = '', 11 | }) => { 12 | return ` 13 | import React, { Component } from 'react'; 14 | import PropTypes from 'prop-types'; 15 | ${imports} 16 | ${composes.map(compose => `import ${compose} from './src/components/${compose}';`).join(os.EOL)} 17 | ${description} 18 | export default class ${componentName} extends Component { 19 | render() { 20 | return
; 21 | } 22 | } 23 | 24 | ${componentName}.propTypes = { 25 | ${composes.map(compose => `...${compose}.propTypes`)}${ 26 | composes.length > 0 ? ',' : '' 27 | }${props.map( 28 | prop => `${ 29 | prop.description 30 | ? `/** 31 | * ${prop.description} 32 | */ ` 33 | : '' 34 | } 35 | ${prop.name}: ${!prop.custom ? 'PropTypes.' : ''}${prop.type}${ 36 | prop.required ? '.isRequired' : '' 37 | }`, 38 | )} 39 | }; 40 | 41 | ${componentName}.defaultProps = { 42 | ${props.map(prop => (prop.default ? `${prop.name}: ${prop.default}` : ''))} 43 | }; 44 | 45 | ${extra}`; 46 | }, 47 | simpleMarkdown: ({ 48 | componentName = 'MyComponent', 49 | description = '', 50 | link = '', 51 | types = [], 52 | isMissing = false, 53 | composes = [], 54 | }) => { 55 | return `## ${componentName}${link}${description} 56 | 57 | ${ 58 | types.length > 0 59 | ? `prop | type | default | required | description 60 | ---- | :----: | :-------: | :--------: | ----------- 61 | ${types 62 | .map( 63 | type => 64 | `**${type.name}** | \`${type.value}\` | ${type.default ? `\`${type.default}\`` : ''} | ${ 65 | type.required ? ':white_check_mark:' : ':x:' 66 | } | ${type.description ? type.description : ''}`, 67 | ) 68 | .join(os.EOL)}` 69 | : 'This component does not have any props.' 70 | } 71 | ${ 72 | isMissing 73 | ? `*Some or all of the composed components are missing from the list below because a documentation couldn't be generated for them. 74 | See the source code of the component for more information.*` 75 | : '' 76 | } 77 | ${ 78 | composes.length > 0 79 | ? `${os.EOL}${componentName} gets more \`propTypes\` from these composed components 80 | ${composes 81 | .map( 82 | compose => `#### ${compose.name} 83 | 84 | ${ 85 | compose.types.length > 0 86 | ? `prop | type | default | required | description 87 | ---- | :----: | :-------: | :--------: | ----------- 88 | ${compose.types 89 | .map( 90 | type => 91 | `**${type.name}** | \`${type.value}\` | ${type.default ? `\`${type.default}\`` : ''} | ${ 92 | type.required ? ':white_check_mark:' : ':x:' 93 | } | ${type.description ? type.description : ''}`, 94 | ) 95 | .join(os.EOL)}` 96 | : 'This component does not have any props.' 97 | }`, 98 | ) 99 | .join(os.EOL + os.EOL)}${os.EOL}` 100 | : '' 101 | }`; 102 | }, 103 | }; 104 | -------------------------------------------------------------------------------- /lib/defaultTemplate.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const { template, type } = require('react-docgen-renderer-template'); 3 | 4 | const generatePropsTable = (props, getType) => { 5 | const entries = Object.entries(props); 6 | if (entries.length === 0) return 'This component does not have any props.'; 7 | 8 | let propsTableHeader = `prop | type | default | required | description 9 | ---- | :----: | :-------: | :--------: | ----------- 10 | `; 11 | return ( 12 | propsTableHeader + 13 | entries 14 | .map( 15 | ([propName, propValue]) => 16 | `**${propName}** | \`${getType(propValue.type)}\` | ${ 17 | propValue.defaultValue ? `\`${propValue.defaultValue}\`` : '' 18 | } | ${propValue.required ? ':white_check_mark:' : ':x:'} | ${ 19 | propValue.description ? propValue.description : '' 20 | }`, 21 | ) 22 | .join(os.EOL) 23 | ); 24 | }; 25 | 26 | const templateCreator = template({ 27 | unknown: 'Unknown', 28 | func: 'Function', 29 | array: 'Array', 30 | object: 'Object', 31 | string: 'String', 32 | number: 'Number', 33 | bool: 'Boolean', 34 | node: 'ReactNode', 35 | element: 'ReactElement', 36 | symbol: 'Symbol', 37 | any: '*', 38 | custom: type`${({ context }) => 39 | context.type.raw.includes('function(') ? '(custom validator)' : context.type.raw}`, 40 | shape: 'Shape', 41 | arrayOf: type`Array[]<${({ context, getType }) => 42 | context.type.value.raw ? context.type.value.raw : getType(context.type.value)}>`, 43 | objectOf: type`Object[#]<${({ context, getType }) => 44 | context.type.value.raw ? context.type.value.raw : getType(context.type.value)}>`, 45 | instanceOf: type`${({ context }) => context.type.value}`, 46 | enum: type`Enum(${({ context, getType }) => 47 | context.type.computed 48 | ? context.type.value 49 | : context.type.value.map(value => getType(value)).join(', ')})`, 50 | union: type`Union<${({ context, getType }) => 51 | context.type.computed 52 | ? context.type.value 53 | : context.type.value.map(value => (value.raw ? value.raw : getType(value))).join('\\|')}>`, 54 | }); 55 | 56 | const templateObject = templateCreator`## ${({ context }) => context.componentName}${({ 57 | context, 58 | }) => { 59 | let headerValue = ''; 60 | if (context.srcLinkUrl) { 61 | headerValue = `${os.EOL}From [\`${context.srcLink}\`](${context.srcLinkUrl})`; 62 | } 63 | 64 | if (context.description) { 65 | headerValue += os.EOL + os.EOL + context.description; 66 | } 67 | 68 | headerValue += os.EOL; 69 | 70 | return headerValue; 71 | }} 72 | ${({ context, getType }) => generatePropsTable(context.props, getType)} 73 | ${({ context }) => 74 | context.isMissingComposes 75 | ? `*Some or all of the composed components are missing from the list below because a documentation couldn't be generated for them. 76 | See the source code of the component for more information.*` 77 | : ''}${({ context, getType }) => 78 | context.composes.length > 0 79 | ? ` 80 | 81 | ${context.componentName} gets more \`propTypes\` from these composed components 82 | ${context.composes 83 | .map( 84 | component => 85 | `#### ${component.componentName} 86 | 87 | ${generatePropsTable(component.props, getType)}`, 88 | ) 89 | .join(os.EOL + os.EOL)}` 90 | : ''} 91 | `; 92 | 93 | module.exports = templateObject; 94 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const os = require('os'); 3 | const process = require('process'); 4 | const defaultTemplate = require('./defaultTemplate'); 5 | 6 | let typeFlatteners = {}; 7 | 8 | const normalizeValue = value => (value ? value.replace(new RegExp(os.EOL, 'g'), ' ') : value); 9 | 10 | const flattenProp = (seed, currentObj, name, isImmediateNesting) => { 11 | let typeObject; 12 | if (currentObj.type && typeof currentObj.type.name === 'string') { 13 | typeObject = currentObj.type; 14 | /* $lab:coverage:off$ */ 15 | } else if (typeof currentObj.name === 'string') { 16 | typeObject = currentObj; 17 | } else if (typeof currentObj === 'string') { 18 | typeObject = { name: currentObj }; 19 | } else { 20 | // This is just a safeguard, shouldn't happen though. 21 | throw new Error(`Unknown object type found for ${name}, check the source code and try again.`); 22 | } 23 | /* $lab:coverage:on$ */ 24 | 25 | (typeFlatteners[typeObject.name] || (() => {}))(seed, typeObject, name); 26 | 27 | if (!isImmediateNesting) { 28 | seed[name] = Object.assign({}, currentObj, { 29 | type: { ...typeObject }, 30 | description: normalizeValue(currentObj.description), 31 | defaultValue: normalizeValue(currentObj.defaultValue && currentObj.defaultValue.value), 32 | }); 33 | } 34 | }; 35 | 36 | typeFlatteners = { 37 | arrayOf(seed, arrayType, name) { 38 | flattenProp(seed, arrayType.value, name + '[]', true); 39 | }, 40 | shape(seed, shapeType, name) { 41 | Object.keys(shapeType.value).forEach(inner => { 42 | flattenProp(seed, shapeType.value[inner], name + '.' + inner); 43 | }); 44 | }, 45 | objectOf(seed, objectOfType, name) { 46 | flattenProp(seed, objectOfType.value, name + '[#]', true); 47 | }, 48 | union(seed, unionType, name) { 49 | if (typeof unionType.value === 'string') { 50 | return; 51 | } 52 | 53 | unionType.value.forEach((type, index) => { 54 | flattenProp(seed, type, name + `<${index + 1}>`); 55 | }); 56 | }, 57 | }; 58 | 59 | const flattenProps = props => { 60 | const sortedProps = {}; 61 | if (props) { 62 | const flattenedProps = Object.keys(props).reduce((seed, prop) => { 63 | flattenProp(seed, props[prop], prop); 64 | return seed; 65 | }, {}); 66 | 67 | Object.keys(flattenedProps) 68 | .sort() 69 | .forEach(key => { 70 | sortedProps[key] = flattenedProps[key]; 71 | }); 72 | } 73 | 74 | return sortedProps; 75 | }; 76 | 77 | class ReactDocGenMarkdownRenderer { 78 | constructor(options) { 79 | this.options = { 80 | componentsBasePath: process.cwd(), 81 | remoteComponentsBasePath: undefined, 82 | template: defaultTemplate, 83 | ...options, 84 | }; 85 | 86 | this.extension = '.md'; 87 | } 88 | 89 | render(file, docs, composes) { 90 | return this.options.template.instantiate( 91 | { 92 | componentName: docs.displayName 93 | ? docs.displayName 94 | : path.basename(file, path.extname(file)), 95 | srcLink: 96 | this.options.remoteComponentsBasePath && 97 | file.replace(this.options.componentsBasePath + path.sep, ''), 98 | srcLinkUrl: 99 | this.options.remoteComponentsBasePath && 100 | file 101 | .replace(this.options.componentsBasePath, this.options.remoteComponentsBasePath) 102 | .replace(/\\/g, '/'), 103 | description: docs.description, 104 | isMissingComposes: (docs.composes || []).length > composes.length, 105 | props: flattenProps(docs.props), 106 | composes: composes.map(compose => ({ 107 | ...compose, 108 | componentName: compose.componentName || compose.displayName, 109 | props: flattenProps(compose.props), 110 | })), 111 | }, 112 | this.extension, 113 | ); 114 | } 115 | } 116 | 117 | ReactDocGenMarkdownRenderer.defaultTemplate = defaultTemplate; 118 | 119 | module.exports = ReactDocGenMarkdownRenderer; 120 | -------------------------------------------------------------------------------- /test/header-render-tests.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('code'); 2 | const Lab = require('lab'); 3 | const lab = (exports.lab = Lab.script()); 4 | const reactDocgen = require('react-docgen'); 5 | const ReactDocGenMarkdownRenderer = require('../lib/index'); 6 | const { simpleComponent, simpleMarkdown } = require('./utils'); 7 | 8 | lab.experiment('header render', () => { 9 | lab.beforeEach(({ context }) => { 10 | context.renderer = new ReactDocGenMarkdownRenderer(); 11 | }); 12 | 13 | lab.test('without description', ({ context }) => { 14 | const result = context.renderer.render( 15 | './some/path', 16 | reactDocgen.parse( 17 | simpleComponent({ 18 | componentName: 'MyComponent', 19 | props: [{ name: 'stringProp', type: 'string' }], 20 | }), 21 | ), 22 | [], 23 | ); 24 | 25 | expect(result).to.equal(simpleMarkdown({ types: [{ name: 'stringProp', value: 'String' }] })); 26 | }); 27 | 28 | lab.test('with description', ({ context }) => { 29 | const result = context.renderer.render( 30 | './some/path', 31 | reactDocgen.parse( 32 | simpleComponent({ 33 | componentName: 'MyComponent', 34 | props: [{ name: 'stringProp', type: 'string' }], 35 | description: `/** 36 | * This is some description for the component. 37 | */`, 38 | }), 39 | ), 40 | [], 41 | ); 42 | 43 | expect(result).to.equal( 44 | simpleMarkdown({ 45 | description: '\n\nThis is some description for the component.', 46 | types: [{ name: 'stringProp', value: 'String' }], 47 | }), 48 | ); 49 | }); 50 | 51 | lab.test('without link', ({ context }) => { 52 | const result = context.renderer.render( 53 | './some/path', 54 | reactDocgen.parse( 55 | simpleComponent({ 56 | componentName: 'MyComponent', 57 | props: [{ name: 'stringProp', type: 'string' }], 58 | }), 59 | ), 60 | [], 61 | ); 62 | 63 | expect(result).to.equal(simpleMarkdown({ types: [{ name: 'stringProp', value: 'String' }] })); 64 | }); 65 | 66 | lab.test('with link', () => { 67 | const renderer = new ReactDocGenMarkdownRenderer({ 68 | componentsBasePath: './some/path', 69 | remoteComponentsBasePath: 70 | 'https://github.com/gen-org/component-library/tree/master/some/path', 71 | }); 72 | 73 | const result = renderer.render( 74 | './some/path/MyComponent.js', 75 | reactDocgen.parse( 76 | simpleComponent({ 77 | componentName: 'MyComponent', 78 | props: [{ name: 'stringProp', type: 'string' }], 79 | }), 80 | ), 81 | [], 82 | ); 83 | 84 | expect(result).to.equal( 85 | simpleMarkdown({ 86 | link: 87 | '\nFrom [`MyComponent.js`](https://github.com/gen-org/component-library/tree/master/some/path/MyComponent.js)', 88 | types: [{ name: 'stringProp', value: 'String' }], 89 | }), 90 | ); 91 | }); 92 | 93 | lab.test('with link & description', () => { 94 | const renderer = new ReactDocGenMarkdownRenderer({ 95 | componentsBasePath: './some/path', 96 | remoteComponentsBasePath: 97 | 'https://github.com/gen-org/component-library/tree/master/some/path', 98 | }); 99 | 100 | const result = renderer.render( 101 | './some/path/MyComponent.js', 102 | reactDocgen.parse( 103 | simpleComponent({ 104 | componentName: 'MyComponent', 105 | props: [{ name: 'stringProp', type: 'string' }], 106 | description: `/** 107 | * This is some description for the component. 108 | */`, 109 | }), 110 | ), 111 | [], 112 | ); 113 | 114 | expect(result).to.equal( 115 | simpleMarkdown({ 116 | description: '\n\nThis is some description for the component.', 117 | link: 118 | '\nFrom [`MyComponent.js`](https://github.com/gen-org/component-library/tree/master/some/path/MyComponent.js)', 119 | types: [{ name: 'stringProp', value: 'String' }], 120 | }), 121 | ); 122 | }); 123 | 124 | lab.test('without props', ({ context }) => { 125 | const result = context.renderer.render( 126 | './some/path', 127 | reactDocgen.parse( 128 | simpleComponent({ 129 | componentName: 'MyComponent', 130 | }), 131 | ), 132 | [], 133 | ); 134 | 135 | expect(result).to.equal(simpleMarkdown({})); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## react-docgen-markdown-renderer 2 | 3 | A simple renderer object that renders a documentation for React components into markdown.
4 | 5 | ### Install 6 | ``` 7 | npm install --save-dev react-docgen-markdown-renderer 8 | ``` 9 | 10 | ### Usage 11 | Once installed, you can use the renderer like the example below 12 | ```javascript 13 | const path = require('path'); 14 | const fs = require('fs'); 15 | const reactDocgen = require('react-docgen'); 16 | const ReactDocGenMarkdownRenderer = require('react-docgen-markdown-renderer'); 17 | const componentPath = path.absolute(path.join(__.dirname, 'components/MyComponent.js')); 18 | const renderer = new ReactDocGenMarkdownRenderer(/* constructor options object */); 19 | 20 | fs.readFile(componentPath, (error, content) => { 21 | const documentationPath = path.basename(componentPath, path.extname(componentPath)) + renderer.extension; 22 | const doc = reactDocgen.parse(content); 23 | fs.writeFile(documentationPath, renderer.render( 24 | /* The path to the component, used for linking to the file. */ 25 | componentPath, 26 | /* The actual react-docgen AST */ 27 | doc, 28 | /* Array of component ASTs that this component composes*/ 29 | [])); 30 | }); 31 | ``` 32 | 33 | #### constructor 34 | **options.componentsBasePath `String`** 35 | This property is optional - defaults to `process.cwd()`.
36 | Represents the base directory where all of your components are stored locally.
37 | It's used as the text for the markdown link at the top of the file - if not specified the link won't appear. 38 | 39 | **options.remoteComponentsBasePath `String`** 40 | This property is optional.
41 | Represents the base directory where all of your components are stored remotely.
42 | It's used as the base url for markdown link at the top of the file - if not specified the link won't appear. 43 | 44 | **options.template `TemplateObject`** 45 | This property is optional - uses the default template if not specified.
46 | A template should be an object constrcuted through the `template` method coming from `react-docgen-renderer-template`. 47 | The default template is 48 | ```javascript 49 | const defaultTemplate = ` 50 | ## {{componentName}} 51 | 52 | {{#if srcLink }}From [\`{{srcLink}}\`]({{srcLink}}){{/if}} 53 | 54 | {{#if description}}{{{description}}}{{/if}} 55 | 56 | prop | type | default | required | description 57 | ---- | :----: | :-------: | :--------: | ----------- 58 | {{#each props}} 59 | **{{@key}}** | \`{{> (typePartial this) this}}\` | {{#if this.defaultValue}}\`{{{this.defaultValue}}}\`{{/if}} | {{#if this.required}}:white_check_mark:{{else}}:x:{{/if}} | {{#if this.description}}{{{this.description}}}{{/if}} 60 | {{/each}} 61 | 62 | {{#if isMissingComposes}} 63 | *Some or all of the composed components are missing from the list below because a documentation couldn't be generated for them. 64 | See the source code of the component for more information.* 65 | {{/if}} 66 | 67 | {{#if composes.length}} 68 | {{componentName}} gets more \`propTypes\` from these composed components 69 | {{/if}} 70 | 71 | {{#each composes}} 72 | #### {{this.componentName}} 73 | 74 | prop | type | default | required | description 75 | ---- | :----: | :-------: | :--------: | ----------- 76 | {{#each this.props}} 77 | **{{@key}}** | \`{{> (typePartial this) this}}\` | {{#if this.defaultValue}}\`{{{this.defaultValue}}}\`{{/if}} | {{#if this.required}}:white_check_mark:{{else}}:x:{{/if}} | {{#if this.description}}{{{this.description}}}{{/if}} 78 | {{/each}} 79 | 80 | {{/each}} 81 | `; 82 | ``` 83 | 84 | ### Example 85 | #### input 86 | ```javascript 87 | /** 88 | * This is an example component. 89 | **/ 90 | class MyComponent extends Component { 91 | 92 | constructor(props) { 93 | super(props); 94 | } 95 | 96 | getDefaultProps() { 97 | return { 98 | enm: 'Photos', 99 | one: { 100 | some: 1, 101 | type: 2, 102 | of: 3, 103 | value: 4 104 | } 105 | }; 106 | } 107 | 108 | render() { 109 | return
; 110 | } 111 | } 112 | 113 | MyComponent.propTypes = { 114 | /** 115 | * A simple `objectOf` propType. 116 | */ 117 | one: React.PropTypes.objectOf(React.PropTypes.number), 118 | /** 119 | * A very complex `objectOf` propType. 120 | */ 121 | two: React.PropTypes.objectOf(React.PropTypes.shape({ 122 | /** 123 | * Just an internal propType for a shape. 124 | * It's also required, and as you can see it supports multi-line comments! 125 | */ 126 | id: React.PropTypes.number.isRequired, 127 | /** 128 | * A simple non-required function 129 | */ 130 | func: React.PropTypes.func, 131 | /** 132 | * An `arrayOf` shape 133 | */ 134 | arr: React.PropTypes.arrayOf(React.PropTypes.shape({ 135 | /** 136 | * 5-level deep propType definition and still works. 137 | */ 138 | index: React.PropTypes.number.isRequired 139 | })) 140 | })), 141 | /** 142 | * `instanceOf` is also supported and the custom type will be shown instead of `instanceOf` 143 | */ 144 | msg: React.PropTypes.instanceOf(Message), 145 | /** 146 | * `oneOf` is basically an Enum which is also supported but can be pretty big. 147 | */ 148 | enm: React.PropTypes.oneOf([print('News'), 'Photos']), 149 | /** 150 | * A multi-type prop is also valid and is displayed as `Union` 151 | */ 152 | union: React.PropTypes.oneOfType([ 153 | React.PropTypes.string, 154 | React.PropTypes.instanceOf(Message) 155 | ]) 156 | }; 157 | ``` 158 | #### output 159 | Example markdown output 160 | 161 | ### FAQ 162 | #### What is this weird type notation? 163 | Well, I wanted to create a table for all of my props. Which means that I can't easily nest the components according to their actual structure.
164 | So this notation is helping define the needed types in a flattened manner. 165 | * `[]` - An `arrayOf` notation. 166 | * `.` - A `shape` notation. 167 | * `[#]` - An `objectOf` notation. 168 | * `<{number}>` - A `union` notation, where the `number` indicates the index of the option in the union. 169 | 170 | In case of `arrayOf`, `objectOf` and `oneOfType` there also exists the internal type of each value which is noted with `<>`. 171 | #### I want to create my own renderer 172 | This is not as hard as it sounds, but there are some things that you have to know.
173 | A renderer has an `extension` property, a `render(file, doc, composes) => String` and `compile(options) => void` (as described above) functions.
174 | Once you have these you're basically done.

175 | `react-docgen-markdown-renderer` expects a `react-docgen` documentation object which helps populate the template above.
176 | It's highly recommended that you use it as well, but note that it doesn't flatten the props by default. 177 | Since you're writing your own renderer you won't have access to all the partials and helpers defined here, but you have the freedom to create your own!

178 | 179 | For more you can always look at the code or open an issue :) 180 | -------------------------------------------------------------------------------- /test/composes-render-tests.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('code'); 2 | const Lab = require('lab'); 3 | const lab = (exports.lab = Lab.script()); 4 | const reactDocgen = require('react-docgen'); 5 | const ReactDocGenMarkdownRenderer = require('../lib/index'); 6 | const { simpleComponent, simpleMarkdown } = require('./utils'); 7 | 8 | lab.experiment('compose render', () => { 9 | lab.beforeEach(({ context }) => { 10 | context.renderer = new ReactDocGenMarkdownRenderer(); 11 | }); 12 | 13 | lab.test('simple', ({ context }) => { 14 | const result = context.renderer.render( 15 | './some/path', 16 | reactDocgen.parse( 17 | simpleComponent({ 18 | componentName: 'MyComponent', 19 | composes: ['AnotherComponent'], 20 | props: [{ name: 'stringProp', type: 'string' }], 21 | }), 22 | ), 23 | [ 24 | reactDocgen.parse( 25 | simpleComponent({ 26 | componentName: 'AnotherComponent', 27 | props: [{ name: 'numberProp', type: 'number' }], 28 | }), 29 | ), 30 | ], 31 | ); 32 | 33 | expect(result).to.equal( 34 | simpleMarkdown({ 35 | types: [{ name: 'stringProp', value: 'String' }], 36 | composes: [{ name: 'AnotherComponent', types: [{ name: 'numberProp', value: 'Number' }] }], 37 | }), 38 | ); 39 | }); 40 | 41 | lab.test('without props', ({ context }) => { 42 | const result = context.renderer.render( 43 | './some/path', 44 | reactDocgen.parse( 45 | simpleComponent({ 46 | componentName: 'MyComponent', 47 | composes: ['AnotherComponent'], 48 | props: [{ name: 'stringProp', type: 'string' }], 49 | }), 50 | ), 51 | [ 52 | reactDocgen.parse( 53 | simpleComponent({ 54 | componentName: 'AnotherComponent', 55 | }), 56 | ), 57 | ], 58 | ); 59 | 60 | expect(result).to.equal( 61 | simpleMarkdown({ 62 | types: [{ name: 'stringProp', value: 'String' }], 63 | composes: [{ name: 'AnotherComponent', types: [] }], 64 | }), 65 | ); 66 | }); 67 | 68 | lab.test('multi', ({ context }) => { 69 | const result = context.renderer.render( 70 | './some/path', 71 | reactDocgen.parse( 72 | simpleComponent({ 73 | componentName: 'MyComponent', 74 | composes: ['AnotherComponent', 'AndAnother'], 75 | props: [{ name: 'stringProp', type: 'string' }], 76 | }), 77 | ), 78 | [ 79 | reactDocgen.parse( 80 | simpleComponent({ 81 | componentName: 'AnotherComponent', 82 | props: [{ name: 'numberProp', type: 'number' }], 83 | }), 84 | ), 85 | reactDocgen.parse( 86 | simpleComponent({ 87 | componentName: 'AndAnother', 88 | props: [{ name: 'boolProp', type: 'bool' }], 89 | }), 90 | ), 91 | ], 92 | ); 93 | 94 | expect(result).to.equal( 95 | simpleMarkdown({ 96 | types: [{ name: 'stringProp', value: 'String' }], 97 | composes: [ 98 | { name: 'AnotherComponent', types: [{ name: 'numberProp', value: 'Number' }] }, 99 | { name: 'AndAnother', types: [{ name: 'boolProp', value: 'Boolean' }] }, 100 | ], 101 | }), 102 | ); 103 | }); 104 | 105 | lab.test('missing', ({ context }) => { 106 | const result = context.renderer.render( 107 | './some/path', 108 | reactDocgen.parse( 109 | simpleComponent({ 110 | componentName: 'MyComponent', 111 | composes: ['AnotherComponent'], 112 | props: [{ name: 'stringProp', type: 'string' }], 113 | }), 114 | ), 115 | [], 116 | ); 117 | 118 | expect(result).to.equal( 119 | simpleMarkdown({ types: [{ name: 'stringProp', value: 'String' }], isMissing: true }), 120 | ); 121 | }); 122 | 123 | lab.test('imported types', ({ context }) => { 124 | const result = context.renderer.render( 125 | './some/path', 126 | reactDocgen.parse( 127 | simpleComponent({ 128 | imports: "import { customImportedShape } from './customImportedShape.js'", 129 | componentName: 'MyComponent', 130 | props: [{ name: 'customImportedShape', custom: true, type: 'customImportedShape' }], 131 | }), 132 | ), 133 | [], 134 | ); 135 | 136 | expect(result).to.equal( 137 | simpleMarkdown({ types: [{ name: 'customImportedShape', value: 'customImportedShape' }] }), 138 | ); 139 | }); 140 | 141 | lab.test('nested imported types', ({ context }) => { 142 | const result = context.renderer.render( 143 | './some/path', 144 | reactDocgen.parse( 145 | simpleComponent({ 146 | imports: "import { customImportedShape } from './customImportedShape.js'", 147 | componentName: 'MyComponent', 148 | props: [ 149 | { 150 | name: 'nestedImportedShape', 151 | custom: true, 152 | type: 153 | 'PropTypes.oneOfType([PropTypes.arrayOf(customImportedShape), customImportedShape])', 154 | }, 155 | ], 156 | }), 157 | ), 158 | [], 159 | ); 160 | 161 | expect(result).to.equal( 162 | simpleMarkdown({ 163 | types: [ 164 | { 165 | name: 'nestedImportedShape', 166 | value: 'Union\\|customImportedShape>', 167 | }, 168 | { name: 'nestedImportedShape<1>', value: 'Array[]' }, 169 | { name: 'nestedImportedShape<2>', value: 'customImportedShape' }, 170 | ], 171 | }), 172 | ); 173 | }); 174 | 175 | lab.test('extra', ({ context }) => { 176 | const result = context.renderer.render( 177 | './some/path', 178 | reactDocgen.parse( 179 | simpleComponent({ 180 | componentName: 'MyComponent', 181 | props: [{ name: 'stringProp', type: 'string' }], 182 | }), 183 | ), 184 | [ 185 | reactDocgen.parse( 186 | simpleComponent({ 187 | componentName: 'AnotherComponent', 188 | props: [{ name: 'numberProp', type: 'number' }], 189 | }), 190 | ), 191 | ], 192 | ); 193 | 194 | expect(result).to.equal( 195 | simpleMarkdown({ 196 | types: [{ name: 'stringProp', value: 'String' }], 197 | composes: [{ name: 'AnotherComponent', types: [{ name: 'numberProp', value: 'Number' }] }], 198 | }), 199 | ); 200 | }); 201 | 202 | lab.test('custom componentName', ({ context }) => { 203 | const anotherComponent = reactDocgen.parse( 204 | simpleComponent({ 205 | componentName: 'AnotherComponent', 206 | }), 207 | ); 208 | anotherComponent.componentName = 'CustomComponentName'; 209 | const result = context.renderer.render( 210 | './some/path', 211 | reactDocgen.parse( 212 | simpleComponent({ 213 | componentName: 'MyComponent', 214 | composes: ['AnotherComponent'], 215 | props: [{ name: 'stringProp', type: 'string' }], 216 | }), 217 | ), 218 | [anotherComponent], 219 | ); 220 | 221 | expect(result).to.equal( 222 | simpleMarkdown({ 223 | types: [{ name: 'stringProp', value: 'String' }], 224 | composes: [{ name: 'CustomComponentName', types: [] }], 225 | }), 226 | ); 227 | }); 228 | }); 229 | -------------------------------------------------------------------------------- /test/prop-metadata-render-tests.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('code'); 2 | const Lab = require('lab'); 3 | const lab = (exports.lab = Lab.script()); 4 | const reactDocgen = require('react-docgen'); 5 | const ReactDocGenMarkdownRenderer = require('../lib/index'); 6 | const { simpleComponent, simpleMarkdown } = require('./utils'); 7 | 8 | lab.experiment('prop metadata render', () => { 9 | lab.beforeEach(({ context }) => { 10 | context.renderer = new ReactDocGenMarkdownRenderer(); 11 | }); 12 | 13 | lab.test('description', ({ context }) => { 14 | const result = context.renderer.render( 15 | './some/path', 16 | reactDocgen.parse( 17 | simpleComponent({ 18 | componentName: 'MyComponent', 19 | props: [ 20 | { 21 | name: 'stringProp', 22 | type: 'string', 23 | description: 'This is a custom required string prop with default', 24 | }, 25 | ], 26 | }), 27 | ), 28 | [], 29 | ); 30 | 31 | expect(result).to.equal( 32 | simpleMarkdown({ 33 | types: [ 34 | { 35 | name: 'stringProp', 36 | value: 'String', 37 | description: 'This is a custom required string prop with default', 38 | }, 39 | ], 40 | }), 41 | ); 42 | }); 43 | 44 | lab.test('required', ({ context }) => { 45 | const result = context.renderer.render( 46 | './some/path', 47 | reactDocgen.parse( 48 | simpleComponent({ 49 | componentName: 'MyComponent', 50 | props: [{ name: 'stringProp', type: 'string', required: true }], 51 | }), 52 | ), 53 | [], 54 | ); 55 | 56 | expect(result).to.equal( 57 | simpleMarkdown({ types: [{ name: 'stringProp', value: 'String', required: true }] }), 58 | ); 59 | }); 60 | 61 | lab.test('default', ({ context }) => { 62 | const result = context.renderer.render( 63 | './some/path', 64 | reactDocgen.parse( 65 | simpleComponent({ 66 | componentName: 'MyComponent', 67 | props: [{ name: 'stringProp', type: 'string', default: "'value'" }], 68 | }), 69 | ), 70 | [], 71 | ); 72 | 73 | expect(result).to.equal( 74 | simpleMarkdown({ types: [{ name: 'stringProp', value: 'String', default: "'value'" }] }), 75 | ); 76 | }); 77 | 78 | lab.test('func type', ({ context }) => { 79 | const result = context.renderer.render( 80 | './some/path', 81 | reactDocgen.parse( 82 | simpleComponent({ 83 | componentName: 'MyComponent', 84 | props: [{ name: 'funcProp', type: 'func' }], 85 | }), 86 | ), 87 | [], 88 | ); 89 | 90 | expect(result).to.equal(simpleMarkdown({ types: [{ name: 'funcProp', value: 'Function' }] })); 91 | }); 92 | 93 | lab.test('array type', ({ context }) => { 94 | const result = context.renderer.render( 95 | './some/path', 96 | reactDocgen.parse( 97 | simpleComponent({ 98 | componentName: 'MyComponent', 99 | props: [{ name: 'arrayProp', type: 'array' }], 100 | }), 101 | ), 102 | [], 103 | ); 104 | 105 | expect(result).to.equal(simpleMarkdown({ types: [{ name: 'arrayProp', value: 'Array' }] })); 106 | }); 107 | 108 | lab.test('object type', ({ context }) => { 109 | const result = context.renderer.render( 110 | './some/path', 111 | reactDocgen.parse( 112 | simpleComponent({ 113 | componentName: 'MyComponent', 114 | props: [{ name: 'objectProp', type: 'object' }], 115 | }), 116 | ), 117 | [], 118 | ); 119 | 120 | expect(result).to.equal(simpleMarkdown({ types: [{ name: 'objectProp', value: 'Object' }] })); 121 | }); 122 | 123 | lab.test('symbol type', ({ context }) => { 124 | const result = context.renderer.render( 125 | './some/path', 126 | reactDocgen.parse( 127 | simpleComponent({ 128 | componentName: 'MyComponent', 129 | props: [{ name: 'symbolProp', type: 'symbol' }], 130 | }), 131 | ), 132 | [], 133 | ); 134 | 135 | expect(result).to.equal(simpleMarkdown({ types: [{ name: 'symbolProp', value: 'Symbol' }] })); 136 | }); 137 | 138 | lab.test('any type', ({ context }) => { 139 | const result = context.renderer.render( 140 | './some/path', 141 | reactDocgen.parse( 142 | simpleComponent({ 143 | componentName: 'MyComponent', 144 | props: [{ name: 'anyProp', type: 'any' }], 145 | }), 146 | ), 147 | [], 148 | ); 149 | 150 | expect(result).to.equal(simpleMarkdown({ types: [{ name: 'anyProp', value: '*' }] })); 151 | }); 152 | 153 | lab.test('node type', ({ context }) => { 154 | const result = context.renderer.render( 155 | './some/path', 156 | reactDocgen.parse( 157 | simpleComponent({ 158 | componentName: 'MyComponent', 159 | props: [{ name: 'nodeProp', type: 'node' }], 160 | }), 161 | ), 162 | [], 163 | ); 164 | 165 | expect(result).to.equal(simpleMarkdown({ types: [{ name: 'nodeProp', value: 'ReactNode' }] })); 166 | }); 167 | 168 | lab.test('element type', ({ context }) => { 169 | const result = context.renderer.render( 170 | './some/path', 171 | reactDocgen.parse( 172 | simpleComponent({ 173 | componentName: 'MyComponent', 174 | props: [{ name: 'elementProp', type: 'element' }], 175 | }), 176 | ), 177 | [], 178 | ); 179 | 180 | expect(result).to.equal( 181 | simpleMarkdown({ types: [{ name: 'elementProp', value: 'ReactElement' }] }), 182 | ); 183 | }); 184 | 185 | lab.test('instanceOf type', ({ context }) => { 186 | const result = context.renderer.render( 187 | './some/path', 188 | reactDocgen.parse( 189 | simpleComponent({ 190 | componentName: 'MyComponent', 191 | props: [{ name: 'instanceOfProp', type: 'instanceOf(Message)' }], 192 | }), 193 | ), 194 | [], 195 | ); 196 | 197 | expect(result).to.equal( 198 | simpleMarkdown({ types: [{ name: 'instanceOfProp', value: 'Message' }] }), 199 | ); 200 | }); 201 | 202 | lab.test('enum type', ({ context }) => { 203 | const result = context.renderer.render( 204 | './some/path', 205 | reactDocgen.parse( 206 | simpleComponent({ 207 | componentName: 'MyComponent', 208 | props: [{ name: 'oneOfProp', type: `oneOf(['Some', 'values', 1, 2 ])` }], 209 | }), 210 | ), 211 | [], 212 | ); 213 | 214 | expect(result).to.equal( 215 | simpleMarkdown({ types: [{ name: 'oneOfProp', value: "Enum('Some', 'values', 1, 2)" }] }), 216 | ); 217 | }); 218 | 219 | lab.test('union type', ({ context }) => { 220 | const result = context.renderer.render( 221 | './some/path', 222 | reactDocgen.parse( 223 | simpleComponent({ 224 | componentName: 'MyComponent', 225 | props: [{ name: 'unionProp', type: 'oneOfType([PropTypes.string, PropTypes.number])' }], 226 | }), 227 | ), 228 | [], 229 | ); 230 | 231 | expect(result).to.equal( 232 | simpleMarkdown({ 233 | types: [ 234 | { name: 'unionProp', value: 'Union' }, 235 | { name: 'unionProp<1>', value: 'String' }, 236 | { name: 'unionProp<2>', value: 'Number' }, 237 | ], 238 | }), 239 | ); 240 | }); 241 | 242 | lab.test('arrayOf type', ({ context }) => { 243 | const result = context.renderer.render( 244 | './some/path', 245 | reactDocgen.parse( 246 | simpleComponent({ 247 | componentName: 'MyComponent', 248 | props: [{ name: 'arrayOfProp', type: 'arrayOf(PropTypes.number)' }], 249 | }), 250 | ), 251 | [], 252 | ); 253 | 254 | expect(result).to.equal( 255 | simpleMarkdown({ types: [{ name: 'arrayOfProp', value: 'Array[]' }] }), 256 | ); 257 | }); 258 | 259 | lab.test('objectOf type', ({ context }) => { 260 | const result = context.renderer.render( 261 | './some/path', 262 | reactDocgen.parse( 263 | simpleComponent({ 264 | componentName: 'MyComponent', 265 | props: [{ name: 'objectOfProp', type: 'objectOf(PropTypes.number)' }], 266 | }), 267 | ), 268 | [], 269 | ); 270 | 271 | expect(result).to.equal( 272 | simpleMarkdown({ types: [{ name: 'objectOfProp', value: 'Object[#]' }] }), 273 | ); 274 | }); 275 | 276 | lab.test('shape type', ({ context }) => { 277 | const result = context.renderer.render( 278 | './some/path', 279 | reactDocgen.parse( 280 | simpleComponent({ 281 | componentName: 'MyComponent', 282 | props: [{ name: 'shapeProp', type: 'shape({ index: PropTypes.number })' }], 283 | }), 284 | ), 285 | [], 286 | ); 287 | 288 | expect(result).to.equal( 289 | simpleMarkdown({ 290 | types: [ 291 | { name: 'shapeProp', value: 'Shape' }, 292 | { name: 'shapeProp.index', value: 'Number' }, 293 | ], 294 | }), 295 | ); 296 | }); 297 | 298 | lab.test('custom type', ({ context }) => { 299 | const result = context.renderer.render( 300 | './some/path', 301 | reactDocgen.parse( 302 | simpleComponent({ 303 | componentName: 'MyComponent', 304 | props: [ 305 | { 306 | name: 'customProp', 307 | type: 'function(props, propName, componentName){}', 308 | custom: true, 309 | }, 310 | ], 311 | }), 312 | ), 313 | [], 314 | ); 315 | 316 | expect(result).to.equal( 317 | simpleMarkdown({ types: [{ name: 'customProp', value: '(custom validator)' }] }), 318 | ); 319 | }); 320 | }); 321 | -------------------------------------------------------------------------------- /test/complex-props-render-tests.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('code'); 2 | const Lab = require('lab'); 3 | const lab = (exports.lab = Lab.script()); 4 | const reactDocgen = require('react-docgen'); 5 | const ReactDocGenMarkdownRenderer = require('../lib/index'); 6 | const { simpleComponent, simpleMarkdown } = require('./utils'); 7 | 8 | lab.experiment('complex render', () => { 9 | lab.beforeEach(({ context }) => { 10 | context.renderer = new ReactDocGenMarkdownRenderer(); 11 | }); 12 | 13 | lab.experiment('union', () => { 14 | lab.beforeEach(({ context }) => { 15 | context.getUnionProp = types => ({ 16 | name: 'unionProp', 17 | type: `oneOfType([${types.map(type => `PropTypes.${type}`)}])`, 18 | }); 19 | }); 20 | 21 | lab.test('with arrayOf', ({ context }) => { 22 | const result = context.renderer.render( 23 | './some/path', 24 | reactDocgen.parse( 25 | simpleComponent({ 26 | componentName: 'MyComponent', 27 | props: [context.getUnionProp(['string', 'arrayOf(PropTypes.number)'])], 28 | }), 29 | ), 30 | [], 31 | ); 32 | 33 | expect(result).to.equal( 34 | simpleMarkdown({ 35 | types: [ 36 | { name: 'unionProp', value: 'Union>' }, 37 | { name: 'unionProp<1>', value: 'String' }, 38 | { name: 'unionProp<2>', value: 'Array[]' }, 39 | ], 40 | }), 41 | ); 42 | }); 43 | 44 | lab.test('with objectOf', ({ context }) => { 45 | const result = context.renderer.render( 46 | './some/path', 47 | reactDocgen.parse( 48 | simpleComponent({ 49 | componentName: 'MyComponent', 50 | props: [context.getUnionProp(['string', 'objectOf(PropTypes.number)'])], 51 | }), 52 | ), 53 | [], 54 | ); 55 | 56 | expect(result).to.equal( 57 | simpleMarkdown({ 58 | types: [ 59 | { name: 'unionProp', value: 'Union>' }, 60 | { name: 'unionProp<1>', value: 'String' }, 61 | { name: 'unionProp<2>', value: 'Object[#]' }, 62 | ], 63 | }), 64 | ); 65 | }); 66 | 67 | lab.test('with shape', ({ context }) => { 68 | const result = context.renderer.render( 69 | './some/path', 70 | reactDocgen.parse( 71 | simpleComponent({ 72 | componentName: 'MyComponent', 73 | props: [context.getUnionProp(['string', 'shape({ index: PropTypes.number })'])], 74 | }), 75 | ), 76 | [], 77 | ); 78 | 79 | expect(result).to.equal( 80 | simpleMarkdown({ 81 | types: [ 82 | { name: 'unionProp', value: 'Union' }, 83 | { name: 'unionProp<1>', value: 'String' }, 84 | { name: 'unionProp<2>', value: 'Shape' }, 85 | { name: 'unionProp<2>.index', value: 'Number' }, 86 | ], 87 | }), 88 | ); 89 | }); 90 | 91 | lab.test('with union', ({ context }) => { 92 | const result = context.renderer.render( 93 | './some/path', 94 | reactDocgen.parse( 95 | simpleComponent({ 96 | componentName: 'MyComponent', 97 | props: [ 98 | context.getUnionProp(['string', 'oneOfType([PropTypes.string, PropTypes.number])']), 99 | ], 100 | }), 101 | ), 102 | [], 103 | ); 104 | 105 | expect(result).to.equal( 106 | simpleMarkdown({ 107 | types: [ 108 | { name: 'unionProp', value: 'Union>' }, 109 | { name: 'unionProp<1>', value: 'String' }, 110 | { name: 'unionProp<2>', value: 'Union' }, 111 | { name: 'unionProp<2><1>', value: 'String' }, 112 | { name: 'unionProp<2><2>', value: 'Number' }, 113 | ], 114 | }), 115 | ); 116 | }); 117 | }); 118 | 119 | lab.experiment('arrayOf', () => { 120 | lab.beforeEach(({ context }) => { 121 | context.getArrayOfProp = type => ({ name: 'arrayOfProp', type: `arrayOf(${type})` }); 122 | }); 123 | 124 | lab.test('with arrayOf', ({ context }) => { 125 | const result = context.renderer.render( 126 | './some/path', 127 | reactDocgen.parse( 128 | simpleComponent({ 129 | componentName: 'MyComponent', 130 | props: [context.getArrayOfProp('arrayOf(PropTypes.number)')], 131 | }), 132 | ), 133 | [], 134 | ); 135 | 136 | expect(result).to.equal( 137 | simpleMarkdown({ types: [{ name: 'arrayOfProp', value: 'Array[]>' }] }), 138 | ); 139 | }); 140 | 141 | lab.test('with objectOf', ({ context }) => { 142 | const result = context.renderer.render( 143 | './some/path', 144 | reactDocgen.parse( 145 | simpleComponent({ 146 | componentName: 'MyComponent', 147 | props: [context.getArrayOfProp('objectOf(PropTypes.number)')], 148 | }), 149 | ), 150 | [], 151 | ); 152 | 153 | expect(result).to.equal( 154 | simpleMarkdown({ types: [{ name: 'arrayOfProp', value: 'Array[]>' }] }), 155 | ); 156 | }); 157 | 158 | lab.test('with shape', ({ context }) => { 159 | const result = context.renderer.render( 160 | './some/path', 161 | reactDocgen.parse( 162 | simpleComponent({ 163 | componentName: 'MyComponent', 164 | props: [context.getArrayOfProp('shape({ index: PropTypes.number })')], 165 | }), 166 | ), 167 | [], 168 | ); 169 | 170 | expect(result).to.equal( 171 | simpleMarkdown({ 172 | types: [ 173 | { name: 'arrayOfProp', value: 'Array[]' }, 174 | { name: 'arrayOfProp[].index', value: 'Number' }, 175 | ], 176 | }), 177 | ); 178 | }); 179 | 180 | lab.test('with union', ({ context }) => { 181 | const result = context.renderer.render( 182 | './some/path', 183 | reactDocgen.parse( 184 | simpleComponent({ 185 | componentName: 'MyComponent', 186 | props: [context.getArrayOfProp('oneOfType([PropTypes.string, PropTypes.number])')], 187 | }), 188 | ), 189 | [], 190 | ); 191 | 192 | expect(result).to.equal( 193 | simpleMarkdown({ 194 | types: [ 195 | { name: 'arrayOfProp', value: 'Array[]>' }, 196 | { name: 'arrayOfProp[]<1>', value: 'String' }, 197 | { name: 'arrayOfProp[]<2>', value: 'Number' }, 198 | ], 199 | }), 200 | ); 201 | }); 202 | }); 203 | 204 | lab.experiment('objectOf', () => { 205 | lab.beforeEach(({ context }) => { 206 | context.getObjectOfProp = type => ({ name: 'objectOfProp', type: `objectOf(${type})` }); 207 | }); 208 | 209 | lab.test('with arrayOf', ({ context }) => { 210 | const result = context.renderer.render( 211 | './some/path', 212 | reactDocgen.parse( 213 | simpleComponent({ 214 | componentName: 'MyComponent', 215 | props: [context.getObjectOfProp('arrayOf(PropTypes.number)')], 216 | }), 217 | ), 218 | [], 219 | ); 220 | 221 | expect(result).to.equal( 222 | simpleMarkdown({ types: [{ name: 'objectOfProp', value: 'Object[#]>' }] }), 223 | ); 224 | }); 225 | 226 | lab.test('with objectOf', ({ context }) => { 227 | const result = context.renderer.render( 228 | './some/path', 229 | reactDocgen.parse( 230 | simpleComponent({ 231 | componentName: 'MyComponent', 232 | props: [context.getObjectOfProp('objectOf(PropTypes.number)')], 233 | }), 234 | ), 235 | [], 236 | ); 237 | 238 | expect(result).to.equal( 239 | simpleMarkdown({ 240 | types: [{ name: 'objectOfProp', value: 'Object[#]>' }], 241 | }), 242 | ); 243 | }); 244 | 245 | lab.test('with shape', ({ context }) => { 246 | const result = context.renderer.render( 247 | './some/path', 248 | reactDocgen.parse( 249 | simpleComponent({ 250 | componentName: 'MyComponent', 251 | props: [context.getObjectOfProp('shape({ index: PropTypes.number })')], 252 | }), 253 | ), 254 | [], 255 | ); 256 | 257 | expect(result).to.equal( 258 | simpleMarkdown({ 259 | types: [ 260 | { name: 'objectOfProp', value: 'Object[#]' }, 261 | { name: 'objectOfProp[#].index', value: 'Number' }, 262 | ], 263 | }), 264 | ); 265 | }); 266 | 267 | lab.test('with union', ({ context }) => { 268 | const result = context.renderer.render( 269 | './some/path', 270 | reactDocgen.parse( 271 | simpleComponent({ 272 | componentName: 'MyComponent', 273 | props: [context.getObjectOfProp('oneOfType([PropTypes.string, PropTypes.number])')], 274 | }), 275 | ), 276 | [], 277 | ); 278 | 279 | expect(result).to.equal( 280 | simpleMarkdown({ 281 | types: [ 282 | { name: 'objectOfProp', value: 'Object[#]>' }, 283 | { name: 'objectOfProp[#]<1>', value: 'String' }, 284 | { name: 'objectOfProp[#]<2>', value: 'Number' }, 285 | ], 286 | }), 287 | ); 288 | }); 289 | }); 290 | 291 | lab.experiment('shape', () => { 292 | lab.beforeEach(({ context }) => { 293 | context.getShapeProp = types => ({ 294 | name: 'shapeProp', 295 | type: `shape({ ${types.map(type => `${type.name}: PropTypes.${type.type}`)} })`, 296 | }); 297 | }); 298 | 299 | lab.test('with arrayOf', ({ context }) => { 300 | const result = context.renderer.render( 301 | './some/path', 302 | reactDocgen.parse( 303 | simpleComponent({ 304 | componentName: 'MyComponent', 305 | props: [ 306 | context.getShapeProp([ 307 | { name: 'stringProp', type: 'string' }, 308 | { name: 'arrayOfProp', type: 'arrayOf(PropTypes.number)' }, 309 | ]), 310 | ], 311 | }), 312 | ), 313 | [], 314 | ); 315 | 316 | expect(result).to.equal( 317 | simpleMarkdown({ 318 | types: [ 319 | { name: 'shapeProp', value: 'Shape' }, 320 | { name: 'shapeProp.arrayOfProp', value: 'Array[]' }, 321 | { name: 'shapeProp.stringProp', value: 'String' }, 322 | ], 323 | }), 324 | ); 325 | }); 326 | 327 | lab.test('with objectOf', ({ context }) => { 328 | const result = context.renderer.render( 329 | './some/path', 330 | reactDocgen.parse( 331 | simpleComponent({ 332 | componentName: 'MyComponent', 333 | props: [ 334 | context.getShapeProp([ 335 | { name: 'stringProp', type: 'string' }, 336 | { name: 'objectOfProp', type: 'objectOf(PropTypes.number)' }, 337 | ]), 338 | ], 339 | }), 340 | ), 341 | [], 342 | ); 343 | 344 | expect(result).to.equal( 345 | simpleMarkdown({ 346 | types: [ 347 | { name: 'shapeProp', value: 'Shape' }, 348 | { name: 'shapeProp.objectOfProp', value: 'Object[#]' }, 349 | { name: 'shapeProp.stringProp', value: 'String' }, 350 | ], 351 | }), 352 | ); 353 | }); 354 | 355 | lab.test('with shape', ({ context }) => { 356 | const result = context.renderer.render( 357 | './some/path', 358 | reactDocgen.parse( 359 | simpleComponent({ 360 | componentName: 'MyComponent', 361 | props: [ 362 | context.getShapeProp([ 363 | { name: 'stringProp', type: 'string' }, 364 | { name: 'shapeProp', type: 'shape({ index: PropTypes.number })' }, 365 | ]), 366 | ], 367 | }), 368 | ), 369 | [], 370 | ); 371 | 372 | expect(result).to.equal( 373 | simpleMarkdown({ 374 | types: [ 375 | { name: 'shapeProp', value: 'Shape' }, 376 | { name: 'shapeProp.shapeProp', value: 'Shape' }, 377 | { name: 'shapeProp.shapeProp.index', value: 'Number' }, 378 | { name: 'shapeProp.stringProp', value: 'String' }, 379 | ], 380 | }), 381 | ); 382 | }); 383 | 384 | lab.test('with union', ({ context }) => { 385 | const result = context.renderer.render( 386 | './some/path', 387 | reactDocgen.parse( 388 | simpleComponent({ 389 | componentName: 'MyComponent', 390 | props: [ 391 | context.getShapeProp([ 392 | { name: 'stringProp', type: 'string' }, 393 | { name: 'unionProp', type: 'oneOfType([PropTypes.string, PropTypes.number])' }, 394 | ]), 395 | ], 396 | }), 397 | ), 398 | [], 399 | ); 400 | 401 | expect(result).to.equal( 402 | simpleMarkdown({ 403 | types: [ 404 | { name: 'shapeProp', value: 'Shape' }, 405 | { name: 'shapeProp.stringProp', value: 'String' }, 406 | { name: 'shapeProp.unionProp', value: 'Union' }, 407 | { name: 'shapeProp.unionProp<1>', value: 'String' }, 408 | { name: 'shapeProp.unionProp<2>', value: 'Number' }, 409 | ], 410 | }), 411 | ); 412 | }); 413 | }); 414 | }); 415 | -------------------------------------------------------------------------------- /test/simple-render-tests.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('code'); 2 | const Lab = require('lab'); 3 | const lab = (exports.lab = Lab.script()); 4 | const reactDocgen = require('react-docgen'); 5 | const ReactDocGenMarkdownRenderer = require('../lib/index'); 6 | const { simpleComponent, simpleMarkdown } = require('./utils'); 7 | 8 | lab.experiment('simple render', () => { 9 | lab.beforeEach(({ context }) => { 10 | context.renderer = new ReactDocGenMarkdownRenderer(); 11 | }); 12 | 13 | lab.test('string type', ({ context }) => { 14 | const result = context.renderer.render( 15 | './some/path', 16 | reactDocgen.parse( 17 | simpleComponent({ 18 | componentName: 'MyComponent', 19 | props: [{ name: 'stringProp', type: 'string' }], 20 | }), 21 | ), 22 | [], 23 | ); 24 | 25 | expect(result).to.equal(simpleMarkdown({ types: [{ name: 'stringProp', value: 'String' }] })); 26 | }); 27 | 28 | lab.test('number type', ({ context }) => { 29 | const result = context.renderer.render( 30 | './some/path', 31 | reactDocgen.parse( 32 | simpleComponent({ 33 | componentName: 'MyComponent', 34 | props: [{ name: 'numProp', type: 'number' }], 35 | }), 36 | ), 37 | [], 38 | ); 39 | 40 | expect(result).to.equal(simpleMarkdown({ types: [{ name: 'numProp', value: 'Number' }] })); 41 | }); 42 | 43 | lab.test('bool type', ({ context }) => { 44 | const result = context.renderer.render( 45 | './some/path', 46 | reactDocgen.parse( 47 | simpleComponent({ 48 | componentName: 'MyComponent', 49 | props: [{ name: 'boolProp', type: 'bool' }], 50 | }), 51 | ), 52 | [], 53 | ); 54 | 55 | expect(result).to.equal(simpleMarkdown({ types: [{ name: 'boolProp', value: 'Boolean' }] })); 56 | }); 57 | 58 | lab.test('func type', ({ context }) => { 59 | const result = context.renderer.render( 60 | './some/path', 61 | reactDocgen.parse( 62 | simpleComponent({ 63 | componentName: 'MyComponent', 64 | props: [{ name: 'funcProp', type: 'func' }], 65 | }), 66 | ), 67 | [], 68 | ); 69 | 70 | expect(result).to.equal(simpleMarkdown({ types: [{ name: 'funcProp', value: 'Function' }] })); 71 | }); 72 | 73 | lab.test('array type', ({ context }) => { 74 | const result = context.renderer.render( 75 | './some/path', 76 | reactDocgen.parse( 77 | simpleComponent({ 78 | componentName: 'MyComponent', 79 | props: [{ name: 'arrayProp', type: 'array' }], 80 | }), 81 | ), 82 | [], 83 | ); 84 | 85 | expect(result).to.equal(simpleMarkdown({ types: [{ name: 'arrayProp', value: 'Array' }] })); 86 | }); 87 | 88 | lab.test('object type', ({ context }) => { 89 | const result = context.renderer.render( 90 | './some/path', 91 | reactDocgen.parse( 92 | simpleComponent({ 93 | componentName: 'MyComponent', 94 | props: [{ name: 'objectProp', type: 'object' }], 95 | }), 96 | ), 97 | [], 98 | ); 99 | 100 | expect(result).to.equal(simpleMarkdown({ types: [{ name: 'objectProp', value: 'Object' }] })); 101 | }); 102 | 103 | lab.test('symbol type', ({ context }) => { 104 | const result = context.renderer.render( 105 | './some/path', 106 | reactDocgen.parse( 107 | simpleComponent({ 108 | componentName: 'MyComponent', 109 | props: [{ name: 'symbolProp', type: 'symbol' }], 110 | }), 111 | ), 112 | [], 113 | ); 114 | 115 | expect(result).to.equal(simpleMarkdown({ types: [{ name: 'symbolProp', value: 'Symbol' }] })); 116 | }); 117 | 118 | lab.test('any type', ({ context }) => { 119 | const result = context.renderer.render( 120 | './some/path', 121 | reactDocgen.parse( 122 | simpleComponent({ 123 | componentName: 'MyComponent', 124 | props: [{ name: 'anyProp', type: 'any' }], 125 | }), 126 | ), 127 | [], 128 | ); 129 | 130 | expect(result).to.equal(simpleMarkdown({ types: [{ name: 'anyProp', value: '*' }] })); 131 | }); 132 | 133 | lab.test('node type', ({ context }) => { 134 | const result = context.renderer.render( 135 | './some/path', 136 | reactDocgen.parse( 137 | simpleComponent({ 138 | componentName: 'MyComponent', 139 | props: [{ name: 'nodeProp', type: 'node' }], 140 | }), 141 | ), 142 | [], 143 | ); 144 | 145 | expect(result).to.equal(simpleMarkdown({ types: [{ name: 'nodeProp', value: 'ReactNode' }] })); 146 | }); 147 | 148 | lab.test('element type', ({ context }) => { 149 | const result = context.renderer.render( 150 | './some/path', 151 | reactDocgen.parse( 152 | simpleComponent({ 153 | componentName: 'MyComponent', 154 | props: [{ name: 'elementProp', type: 'element' }], 155 | }), 156 | ), 157 | [], 158 | ); 159 | 160 | expect(result).to.equal( 161 | simpleMarkdown({ types: [{ name: 'elementProp', value: 'ReactElement' }] }), 162 | ); 163 | }); 164 | 165 | lab.test('instanceOf type', ({ context }) => { 166 | const result = context.renderer.render( 167 | './some/path', 168 | reactDocgen.parse( 169 | simpleComponent({ 170 | componentName: 'MyComponent', 171 | props: [{ name: 'instanceOfProp', type: 'instanceOf(Message)' }], 172 | }), 173 | ), 174 | [], 175 | ); 176 | 177 | expect(result).to.equal( 178 | simpleMarkdown({ types: [{ name: 'instanceOfProp', value: 'Message' }] }), 179 | ); 180 | }); 181 | 182 | lab.test('enum type', ({ context }) => { 183 | const result = context.renderer.render( 184 | './some/path', 185 | reactDocgen.parse( 186 | simpleComponent({ 187 | componentName: 'MyComponent', 188 | props: [{ name: 'oneOfProp', type: `oneOf(['Some', 'values', 1, 2 ])` }], 189 | }), 190 | ), 191 | [], 192 | ); 193 | 194 | expect(result).to.equal( 195 | simpleMarkdown({ types: [{ name: 'oneOfProp', value: "Enum('Some', 'values', 1, 2)" }] }), 196 | ); 197 | }); 198 | 199 | lab.test('union type', ({ context }) => { 200 | const result = context.renderer.render( 201 | './some/path', 202 | reactDocgen.parse( 203 | simpleComponent({ 204 | componentName: 'MyComponent', 205 | props: [{ name: 'unionProp', type: 'oneOfType([PropTypes.string, PropTypes.number])' }], 206 | }), 207 | ), 208 | [], 209 | ); 210 | 211 | expect(result).to.equal( 212 | simpleMarkdown({ 213 | types: [ 214 | { name: 'unionProp', value: 'Union' }, 215 | { name: 'unionProp<1>', value: 'String' }, 216 | { name: 'unionProp<2>', value: 'Number' }, 217 | ], 218 | }), 219 | ); 220 | }); 221 | 222 | lab.test('arrayOf type', ({ context }) => { 223 | const result = context.renderer.render( 224 | './some/path', 225 | reactDocgen.parse( 226 | simpleComponent({ 227 | componentName: 'MyComponent', 228 | props: [{ name: 'arrayOfProp', type: 'arrayOf(PropTypes.number)' }], 229 | }), 230 | ), 231 | [], 232 | ); 233 | 234 | expect(result).to.equal( 235 | simpleMarkdown({ types: [{ name: 'arrayOfProp', value: 'Array[]' }] }), 236 | ); 237 | }); 238 | 239 | lab.test('objectOf type', ({ context }) => { 240 | const result = context.renderer.render( 241 | './some/path', 242 | reactDocgen.parse( 243 | simpleComponent({ 244 | componentName: 'MyComponent', 245 | props: [{ name: 'objectOfProp', type: 'objectOf(PropTypes.number)' }], 246 | }), 247 | ), 248 | [], 249 | ); 250 | 251 | expect(result).to.equal( 252 | simpleMarkdown({ types: [{ name: 'objectOfProp', value: 'Object[#]' }] }), 253 | ); 254 | }); 255 | 256 | lab.test('shape type', ({ context }) => { 257 | const result = context.renderer.render( 258 | './some/path', 259 | reactDocgen.parse( 260 | simpleComponent({ 261 | componentName: 'MyComponent', 262 | props: [{ name: 'shapeProp', type: 'shape({ index: PropTypes.number })' }], 263 | }), 264 | ), 265 | [], 266 | ); 267 | 268 | expect(result).to.equal( 269 | simpleMarkdown({ 270 | types: [ 271 | { name: 'shapeProp', value: 'Shape' }, 272 | { name: 'shapeProp.index', value: 'Number' }, 273 | ], 274 | }), 275 | ); 276 | }); 277 | 278 | lab.test('custom type', ({ context }) => { 279 | const result = context.renderer.render( 280 | './some/path', 281 | reactDocgen.parse( 282 | simpleComponent({ 283 | componentName: 'MyComponent', 284 | props: [ 285 | { 286 | name: 'customProp', 287 | type: 'function(props, propName, componentName){}', 288 | custom: true, 289 | }, 290 | ], 291 | }), 292 | ), 293 | [], 294 | ); 295 | 296 | expect(result).to.equal( 297 | simpleMarkdown({ types: [{ name: 'customProp', value: '(custom validator)' }] }), 298 | ); 299 | }); 300 | 301 | lab.test('arrayOf custom type', ({ context }) => { 302 | const result = context.renderer.render( 303 | './some/path', 304 | reactDocgen.parse( 305 | simpleComponent({ 306 | extra: `MyComponent.customShape = PropTypes.shape({ id: PropTypes.number });`, 307 | componentName: 'MyComponent', 308 | props: [ 309 | { 310 | name: 'customProp', 311 | type: 'PropTypes.arrayOf(MyComponent.customShape)', 312 | custom: true, 313 | }, 314 | ], 315 | }), 316 | ), 317 | [], 318 | ); 319 | 320 | expect(result).to.equal( 321 | simpleMarkdown({ 322 | types: [{ name: 'customProp', value: 'Array[]' }], 323 | }), 324 | ); 325 | }); 326 | 327 | lab.test('objectOf custom type', ({ context }) => { 328 | const result = context.renderer.render( 329 | './some/path', 330 | reactDocgen.parse( 331 | simpleComponent({ 332 | extra: `MyComponent.customShape = PropTypes.shape({ id: PropTypes.number });`, 333 | componentName: 'MyComponent', 334 | props: [ 335 | { 336 | name: 'customProp', 337 | type: 'PropTypes.objectOf(MyComponent.customShape)', 338 | custom: true, 339 | }, 340 | ], 341 | }), 342 | ), 343 | [], 344 | ); 345 | 346 | expect(result).to.equal( 347 | simpleMarkdown({ 348 | types: [{ name: 'customProp', value: 'Object[#]' }], 349 | }), 350 | ); 351 | }); 352 | 353 | lab.test('union custom type', ({ context }) => { 354 | const result = context.renderer.render( 355 | './some/path', 356 | reactDocgen.parse( 357 | simpleComponent({ 358 | extra: `MyComponent.customType = [PropTypes.string, PropTypes.shape({ id: PropTypes.number })];`, 359 | componentName: 'MyComponent', 360 | props: [ 361 | { 362 | name: 'customProp', 363 | type: 'PropTypes.oneOfType(MyComponent.customShape)', 364 | custom: true, 365 | }, 366 | ], 367 | }), 368 | ), 369 | [], 370 | ); 371 | 372 | expect(result).to.equal( 373 | simpleMarkdown({ types: [{ name: 'customProp', value: 'Union' }] }), 374 | ); 375 | }); 376 | 377 | lab.test('union with custom type', ({ context }) => { 378 | const result = context.renderer.render( 379 | './some/path', 380 | reactDocgen.parse( 381 | simpleComponent({ 382 | extra: `MyComponent.customShape = PropTypes.shape({ id: PropTypes.number });`, 383 | componentName: 'MyComponent', 384 | props: [ 385 | { 386 | name: 'customProp', 387 | type: 'PropTypes.oneOfType([PropTypes.string, MyComponent.customShape])', 388 | custom: true, 389 | }, 390 | ], 391 | }), 392 | ), 393 | [], 394 | ); 395 | 396 | expect(result).to.equal( 397 | simpleMarkdown({ 398 | types: [ 399 | { name: 'customProp', value: 'Union' }, 400 | { name: 'customProp<1>', value: 'String' }, 401 | { name: 'customProp<2>', value: 'MyComponent.customShape' }, 402 | ], 403 | }), 404 | ); 405 | }); 406 | 407 | lab.test('enum custom type', ({ context }) => { 408 | const result = context.renderer.render( 409 | './some/path', 410 | reactDocgen.parse( 411 | simpleComponent({ 412 | extra: `MyComponent.customEnum = ['Some', 'values', 1, 2 ];`, 413 | componentName: 'MyComponent', 414 | props: [ 415 | { name: 'oneOfProp', type: `PropTypes.oneOf(MyComponent.customEnum)`, custom: true }, 416 | ], 417 | }), 418 | ), 419 | [], 420 | ); 421 | 422 | expect(result).to.equal( 423 | simpleMarkdown({ types: [{ name: 'oneOfProp', value: 'Enum(MyComponent.customEnum)' }] }), 424 | ); 425 | }); 426 | 427 | lab.test('enum with custom value', ({ context }) => { 428 | const result = context.renderer.render( 429 | './some/path', 430 | reactDocgen.parse( 431 | simpleComponent({ 432 | extra: `MyComponent.customEnumValue = ['Value', 1, 2];`, 433 | componentName: 'MyComponent', 434 | props: [ 435 | { 436 | name: 'oneOfProp', 437 | type: `PropTypes.oneOf(['Some', ...MyComponent.customEnumValue])`, 438 | custom: true, 439 | }, 440 | ], 441 | }), 442 | ), 443 | [], 444 | ); 445 | 446 | expect(result).to.equal( 447 | simpleMarkdown({ 448 | types: [{ name: 'oneOfProp', value: "Enum('Some', ...MyComponent.customEnumValue)" }], 449 | }), 450 | ); 451 | }); 452 | 453 | lab.test('unknown type', ({ context }) => { 454 | const warn = console.warn; 455 | console.warn = () => {}; 456 | 457 | const docgen = reactDocgen.parse( 458 | simpleComponent({ 459 | componentName: 'MyComponent', 460 | props: [ 461 | { name: 'unknownProp', type: 'function(props, propName, componentName){}', custom: true }, 462 | ], 463 | }), 464 | ); 465 | 466 | docgen.props.unknownProp.type.name = 'Unknown Type'; 467 | 468 | const result = context.renderer.render('./some/path', docgen, []); 469 | 470 | expect(result).to.equal(simpleMarkdown({ types: [{ name: 'unknownProp', value: 'Unknown' }] })); 471 | 472 | console.warn = warn; 473 | }); 474 | 475 | lab.test('no displayName', ({ context }) => { 476 | const docgen = reactDocgen.parse(`export default ({ someProp }) =>
`); 477 | 478 | const result = context.renderer.render('./some/path', docgen, []); 479 | 480 | expect(result).to.equal(simpleMarkdown({ componentName: 'path' })); 481 | }); 482 | }); 483 | --------------------------------------------------------------------------------