├── .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
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