├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── index.js ├── package.json ├── readme.md ├── test ├── fixtures │ ├── .eslintrc │ ├── any.js │ ├── array-of-shape.js │ ├── array-of.js │ ├── one-of-type.js │ ├── one-of.js │ └── primitives.js ├── index.js └── snapshots │ ├── index.js.md │ └── index.js.snap └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "standard" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # builds 7 | build 8 | dist 9 | 10 | # misc 11 | .DS_Store 12 | .env 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | .cache 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: yarn 3 | node_js: 4 | - 9 5 | - 8 6 | - 6 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const safeEval = require('notevil') 4 | 5 | /** 6 | * Converts a single JSON object extracted by react-docgen to JSON Schema. 7 | * 8 | * @name reactDocgenToJSONSchema 9 | * @type function 10 | * 11 | * @param {object} input - JSON object documenting a single component. 12 | * 13 | * @return {object} 14 | */ 15 | module.exports = (input) => { 16 | const { 17 | props = { }, 18 | displayName 19 | } = input 20 | 21 | const { 22 | properties, 23 | required 24 | } = getSchemaProperties(props) 25 | 26 | const jsonSchema = { 27 | title: displayName, 28 | type: 'object', 29 | properties 30 | } 31 | 32 | if (required.length) { 33 | jsonSchema.required = required 34 | } 35 | 36 | return jsonSchema 37 | } 38 | 39 | // Extract JSON Schema style properties from react-docgen props. 40 | const getSchemaProperties = (props) => { 41 | const required = [] 42 | 43 | const properties = Object.keys(props).reduce((result, key) => { 44 | const original = props[key] 45 | 46 | // Skip props that have '@ignore' in description (eg. in material-ui) 47 | if (typeof original.description === 'undefined' && typeof original.type !== 'undefined') { 48 | if (original.type.description && original.type.description.indexOf('@ignore') > -1) { 49 | return result 50 | } 51 | } else { 52 | if (original.description.indexOf('@ignore') > -1) { 53 | return result 54 | } 55 | 56 | if (original.required) { 57 | required.push(key) 58 | } 59 | } 60 | 61 | const value = getPropertyForProp(original) 62 | 63 | if (value) { 64 | result[key] = value 65 | } 66 | 67 | return result 68 | }, {}) 69 | 70 | return { 71 | properties, 72 | required 73 | } 74 | } 75 | 76 | const reduceShapeToProps = (shapes) => 77 | Object.keys(shapes).reduce((result, key) => { 78 | result[key] = { type: shapes[key] } 79 | 80 | return result 81 | }, {}) 82 | 83 | // Convert a property extracted by react-docgen to a JSON Schema property. 84 | const getPropertyForProp = ({ 85 | type = { name: '' }, 86 | description, 87 | defaultValue 88 | }) => { 89 | let result = { 90 | type: type.name 91 | } 92 | 93 | if (description) { 94 | result.description = description 95 | } 96 | 97 | if (type.name === 'enum') { 98 | // Only process enums if they're arrays. Don't process them if they are 99 | // references to an object. 100 | if (typeof type.value === 'object') { 101 | result.enum = type.value.reduce((collector, item) => { 102 | try { 103 | const value = safeEval(item.value) 104 | 105 | if (typeof item.value === 'object') { 106 | collector.push({ 107 | value 108 | }) 109 | } else { 110 | collector.push(value) 111 | } 112 | } catch (e) { 113 | console.log('could not evalulate value', e) 114 | return collector 115 | } 116 | 117 | return collector 118 | }, []) 119 | 120 | // type is equal to all those found in the enum 121 | const types = result.enum.map(value => typeof value) 122 | .filter((elem, pos, arr) => arr.indexOf(elem) === pos) 123 | 124 | result.type = types.length === 1 ? types[0] : types 125 | } else { 126 | // Assume a string if not an object. This is because JS is loosely typed, 127 | // so we can normally get away with using a string. 128 | result.type = 'string' 129 | } 130 | } else if (type.name === 'union') { 131 | // Without parsing all schemas together then no ability to inter-reference one- 132 | // another through `"$ref": "#path/to/my/JsonSchemaType"` 133 | result.anyOf = type.value.map((subType) => getPropertyForProp({ type: subType })) 134 | delete result.type 135 | } else if (type.name === 'arrayOf') { 136 | result.type = 'array' 137 | result.items = getPropertyForProp({ type: type.value }) 138 | } else if (type.name === 'shape') { 139 | result.type = 'object' 140 | 141 | const { 142 | properties, 143 | required 144 | } = getSchemaProperties(reduceShapeToProps(type.value)) 145 | 146 | result.properties = properties 147 | if (required.length) { 148 | result.required = required 149 | } 150 | } else if (type.name === 'node') { 151 | result.type = 'object' 152 | } else if (type.name === 'bool') { 153 | result.type = 'boolean' 154 | } else if (type.name === 'func') { 155 | return 156 | } else if (type.name === 'any') { 157 | // for no type definition, do not define type in schema 158 | delete result.type 159 | } 160 | 161 | if (defaultValue) { 162 | try { 163 | result.default = safeEval(defaultValue.value) 164 | } catch (e) { 165 | console.log('could not evalulate defaultValue', defaultValue.value, e.message) 166 | } 167 | } 168 | 169 | return result 170 | } 171 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-docgen-to-json-schema", 3 | "version": "0.2.0", 4 | "description": "Converts react-docgen output to JSON Schema.", 5 | "main": "index.js", 6 | "repository": "hydrateio/react-docgen-to-json-schema", 7 | "author": "Hydrate (https://hydrate.io)", 8 | "license": "MIT", 9 | "scripts": { 10 | "docs": "update-markdown-jsdoc --no-markdown-toc", 11 | "test": "ava && eslint ." 12 | }, 13 | "engines": { 14 | "node": ">=6" 15 | }, 16 | "keywords": [ 17 | "react", 18 | "react-docgen", 19 | "docgen", 20 | "docs", 21 | "documentation", 22 | "transform", 23 | "convert", 24 | "proptypes", 25 | "prop-types", 26 | "json", 27 | "schema", 28 | "json-schema", 29 | "quicktype" 30 | ], 31 | "devDependencies": { 32 | "ajv": "^6.5.3", 33 | "ava": "^0.25.0", 34 | "babel-core": "^6.26.3", 35 | "babel-eslint": "^8.2.6", 36 | "babel-preset-env": "^1.7.0", 37 | "babel-preset-react": "^6.24.1", 38 | "babel-preset-stage-0": "^6.24.1", 39 | "eslint": "^5.4.0", 40 | "eslint-config-standard": "^11.0.0", 41 | "eslint-config-standard-react": "^6.0.0", 42 | "eslint-plugin-import": "^2.14.0", 43 | "eslint-plugin-node": "^7.0.1", 44 | "eslint-plugin-promise": "^4.0.0", 45 | "eslint-plugin-react": "^7.11.1", 46 | "eslint-plugin-standard": "^3.1.0", 47 | "react-docgen": "^2.21.0", 48 | "standard": "^11.0.0", 49 | "update-markdown-jsdoc": "^1.0.6" 50 | }, 51 | "dependencies": { 52 | "notevil": "^1.1.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # react-docgen-to-json-schema 2 | 3 | > Converts [react-docgen]((https://github.com/reactjs/react-docgen)) output to [JSON Schema](http://json-schema.org). 4 | 5 | [![NPM](https://img.shields.io/npm/v/react-docgen-to-json-schema.svg)](https://www.npmjs.com/package/react-docgen-to-json-schema) [![Build Status](https://travis-ci.com/hydrateio/react-docgen-to-json-schema.svg?branch=master)](https://travis-ci.com/hydrateio/react-docgen-to-json-schema) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 6 | 7 | ## Install 8 | 9 | This module requires `node >= 6`. 10 | 11 | ```bash 12 | npm install --save react-docgen-to-json-schema 13 | ``` 14 | 15 | ## Usage 16 | 17 | Take this example React component. 18 | 19 | ```js 20 | import { Component } from 'react' 21 | import PropTypes from 'prop-types' 22 | 23 | /** 24 | * General component description. 25 | */ 26 | export default class MyComponent extends Component { 27 | static propTypes = { 28 | /** 29 | * Description foo. 30 | */ 31 | foo: PropTypes.number.isRequired, 32 | 33 | /** 34 | * Description bar. 35 | * 36 | * - markdown list-item 1 37 | * - markdown list-item 2 38 | */ 39 | bar: PropTypes.string, 40 | 41 | /** 42 | * Description baz. 43 | */ 44 | baz: PropTypes.bool 45 | } 46 | 47 | static defaultProps = { 48 | bar: 'bar' 49 | } 50 | 51 | render: () => { } 52 | } 53 | ``` 54 | 55 | `react-docgen` generates the following JSON: 56 | 57 | ```js 58 | { 59 | "description": "General component description.", 60 | "displayName": "MyComponent", 61 | "methods": [], 62 | "props": { 63 | "foo": { 64 | "type": { 65 | "name": "number" 66 | }, 67 | "required": true, 68 | "description": "Description foo." 69 | }, 70 | "bar": { 71 | "type": { 72 | "name": "string" 73 | }, 74 | "required": false, 75 | "description": "Description bar.\n\n- markdown list-item 1\n- markdown list-item 2", 76 | "defaultValue": { 77 | "value": "'bar'", 78 | "computed": false 79 | } 80 | }, 81 | "baz": { 82 | "type": { 83 | "name": "bool" 84 | }, 85 | "required": false, 86 | "description": "Description baz." 87 | } 88 | } 89 | } 90 | ``` 91 | 92 | `react-docgen-to-json-schema` takes in this JSON and converts it to the following JSON Schema: 93 | 94 | ```js 95 | { 96 | "title": "MyComponent", 97 | "type": "object", 98 | "properties": { 99 | "foo": { 100 | "type": "number", 101 | "description": "Description foo." 102 | }, 103 | "bar": { 104 | "type": "string", 105 | "description": "Description bar.\n\n- markdown list-item 1\n- markdown list-item 2", 106 | "default": "bar" 107 | }, 108 | "baz": { 109 | "type": "boolean", 110 | "description": "Description baz." 111 | } 112 | }, 113 | "required": [ 114 | "foo" 115 | ] 116 | } 117 | ``` 118 | 119 | ## API 120 | 121 | 122 | 123 | ### [reactDocgenToJSONSchema](https://github.com/hydrateio/react-docgen-to-json-schema/blob/03fea6e640aa5aecd5dd6e2e3bc5816518cbb1b8/index.js#L13-L35) 124 | 125 | Converts a single JSON object extracted by react-docgen to JSON Schema. 126 | 127 | - `input` **[object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** JSON object documenting a single component. 128 | 129 | ## Status 130 | 131 | - [PropTypes](https://reactjs.org/docs/typechecking-with-proptypes.html) 132 | - [x] PropTypes.array 133 | - [x] PropTypes.bool 134 | - [ ] PropTypes.func 135 | - [x] PropTypes.number 136 | - [x] PropTypes.object 137 | - [x] PropTypes.string 138 | - [ ] PropTypes.symbol 139 | - [ ] PropTypes.node 140 | - [ ] PropTypes.element 141 | - [ ] PropTypes.instanceOf 142 | - [x] PropTypes.oneOf (enums) 143 | - [ ] PropTypes.oneOfType (unions) 144 | - [x] PropTypes.arrayOf 145 | - [ ] PropTypes.objectOf 146 | - [x] PropTypes.shape 147 | - [ ] PropTypes.any 148 | - [x] PropTypes isRequired 149 | - [ ] PropTypes custom function 150 | - [x] PropTypes default values 151 | 152 | ## Related 153 | 154 | - [JSON Schema](http://json-schema.org) - Official JSON Schema spec. 155 | - [React Docgen](https://github.com/reactjs/react-docgen) - Extracts docs from React source files. 156 | 157 | ## License 158 | 159 | MIT © [Hydrate](https://github.com/hydrateio) 160 | -------------------------------------------------------------------------------- /test/fixtures/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard", 5 | "standard-react" 6 | ], 7 | "env": { 8 | "es6": true 9 | }, 10 | "plugins": [ 11 | "react" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | // don't force es6 functions to include space before paren 18 | "space-before-function-paren": 0, 19 | 20 | // allow specifying true explicitly for boolean props 21 | "react/jsx-boolean-value": 0, 22 | 23 | // disregard unused prop types 24 | "react/no-unused-prop-types": 0 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/fixtures/any.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export default class MyComponent extends Component { 5 | static propTypes = { 6 | /** 7 | * Description foo. 8 | */ 9 | foo: PropTypes.any 10 | } 11 | 12 | static defaultProps = { 13 | foo: 'test' 14 | } 15 | 16 | render: () => { } 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/array-of-shape.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export default class MyComponent extends Component { 5 | static propTypes = { 6 | /** 7 | * Description foo. 8 | */ 9 | foo: PropTypes.arrayOf(PropTypes.shape({ 10 | /** 11 | * Description bar 12 | */ 13 | bar: PropTypes.string, 14 | /** 15 | * Description baz 16 | */ 17 | baz: PropTypes.bool 18 | })) 19 | } 20 | 21 | static defaultProps = { 22 | foo: [ { 23 | bar: 'test bar', 24 | baz: 'test baz' 25 | } ] 26 | } 27 | 28 | render: () => { } 29 | } 30 | -------------------------------------------------------------------------------- /test/fixtures/array-of.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export default class MyComponent extends Component { 5 | static propTypes = { 6 | /** 7 | * Description foo. 8 | */ 9 | foo: PropTypes.arrayOf(PropTypes.string) 10 | } 11 | 12 | static defaultProps = { 13 | foo: [ 'test' ] 14 | } 15 | 16 | render: () => { } 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/one-of-type.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export default class MyComponent extends Component { 5 | static propTypes = { 6 | /** 7 | * Description foo. 8 | */ 9 | foo: PropTypes.oneOfType([ 10 | PropTypes.number, 11 | PropTypes.string 12 | ]) 13 | } 14 | 15 | static defaultProps = { 16 | foo: 42 17 | } 18 | 19 | render: () => { } 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/one-of.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export default class MyComponent extends Component { 5 | static propTypes = { 6 | /** 7 | * Description foo. 8 | */ 9 | foo: PropTypes.oneOf([ 10 | 'red', 11 | "blue", // eslint-disable-line 12 | 'green' 13 | ]), 14 | 15 | /** 16 | * Description bar. 17 | */ 18 | bar: PropTypes.oneOf([ 19 | 'green', 20 | 42, 21 | null 22 | ]) 23 | } 24 | 25 | static defaultProps = { 26 | foo: 'red' 27 | } 28 | 29 | render: () => { } 30 | } 31 | -------------------------------------------------------------------------------- /test/fixtures/primitives.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | /** 5 | * General component description. 6 | */ 7 | export default class MyComponent extends Component { 8 | static propTypes = { 9 | /** 10 | * Description foo. 11 | */ 12 | foo: PropTypes.number.isRequired, 13 | 14 | /** 15 | * Description bar. 16 | * 17 | * - markdown list-item 1 18 | * - markdown list-item 2 19 | */ 20 | bar: PropTypes.string, 21 | 22 | /** 23 | * Description baz. 24 | */ 25 | baz: PropTypes.bool 26 | } 27 | 28 | static defaultProps = { 29 | bar: 'bar' 30 | } 31 | 32 | render: () => { } 33 | } 34 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('ava') 4 | const Ajv = require('ajv') 5 | const fs = require('fs') 6 | const path = require('path') 7 | const docgen = require('react-docgen') 8 | const docgenToJSONSchema = require('..') 9 | 10 | const fixtures = path.join(__dirname, 'fixtures') 11 | 12 | fs.readdirSync(fixtures) 13 | .filter((filename) => { 14 | return /\.(js|ts)$/.test(filename) 15 | }) 16 | .forEach((filename) => { 17 | const inputPath = path.join(fixtures, filename) 18 | test(`${filename}`, (t) => { 19 | const input = fs.readFileSync(inputPath, 'utf8') 20 | const docs = docgen.parse(input) 21 | const schema = docgenToJSONSchema(docs) 22 | console.log(JSON.stringify(schema, null, 2)) 23 | 24 | // ensure resulting schema is a valid JSON Schema 25 | const ajv = new Ajv() 26 | ajv.compile(schema) 27 | 28 | t.snapshot(schema) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/snapshots/index.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/index.js` 2 | 3 | The actual snapshot is saved in `index.js.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## any.js 8 | 9 | > Snapshot 1 10 | 11 | { 12 | properties: { 13 | foo: { 14 | default: 'test', 15 | description: 'Description foo.', 16 | }, 17 | }, 18 | title: 'MyComponent', 19 | type: 'object', 20 | } 21 | 22 | ## array-of-shape.js 23 | 24 | > Snapshot 1 25 | 26 | { 27 | properties: { 28 | foo: { 29 | default: [ 30 | { 31 | bar: 'test bar', 32 | baz: 'test baz', 33 | }, 34 | ], 35 | description: 'Description foo.', 36 | items: { 37 | properties: { 38 | bar: { 39 | type: 'string', 40 | }, 41 | baz: { 42 | type: 'boolean', 43 | }, 44 | }, 45 | type: 'object', 46 | }, 47 | type: 'array', 48 | }, 49 | }, 50 | title: 'MyComponent', 51 | type: 'object', 52 | } 53 | 54 | ## array-of.js 55 | 56 | > Snapshot 1 57 | 58 | { 59 | properties: { 60 | foo: { 61 | default: [ 62 | 'test', 63 | ], 64 | description: 'Description foo.', 65 | items: { 66 | type: 'string', 67 | }, 68 | type: 'array', 69 | }, 70 | }, 71 | title: 'MyComponent', 72 | type: 'object', 73 | } 74 | 75 | ## one-of-type.js 76 | 77 | > Snapshot 1 78 | 79 | { 80 | properties: { 81 | foo: { 82 | anyOf: [ 83 | { 84 | type: 'number', 85 | }, 86 | { 87 | type: 'string', 88 | }, 89 | ], 90 | default: 42, 91 | description: 'Description foo.', 92 | }, 93 | }, 94 | title: 'MyComponent', 95 | type: 'object', 96 | } 97 | 98 | ## one-of.js 99 | 100 | > Snapshot 1 101 | 102 | { 103 | properties: { 104 | bar: { 105 | description: 'Description bar.', 106 | enum: [ 107 | 'green', 108 | 42, 109 | null, 110 | ], 111 | type: [ 112 | 'string', 113 | 'number', 114 | 'object', 115 | ], 116 | }, 117 | foo: { 118 | default: 'red', 119 | description: 'Description foo.', 120 | enum: [ 121 | 'red', 122 | 'blue', 123 | 'green', 124 | ], 125 | type: 'string', 126 | }, 127 | }, 128 | title: 'MyComponent', 129 | type: 'object', 130 | } 131 | 132 | ## primitives.js 133 | 134 | > Snapshot 1 135 | 136 | { 137 | properties: { 138 | bar: { 139 | default: 'bar', 140 | description: `Description bar.␊ 141 | ␊ 142 | - markdown list-item 1␊ 143 | - markdown list-item 2`, 144 | type: 'string', 145 | }, 146 | baz: { 147 | description: 'Description baz.', 148 | type: 'boolean', 149 | }, 150 | foo: { 151 | description: 'Description foo.', 152 | type: 'number', 153 | }, 154 | }, 155 | required: [ 156 | 'foo', 157 | ], 158 | title: 'MyComponent', 159 | type: 'object', 160 | } 161 | -------------------------------------------------------------------------------- /test/snapshots/index.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agenticaivc/react-docgen-to-json-schema/cdffbbd39565ae516bbd9dd7d147b42e7917e870/test/snapshots/index.js.snap --------------------------------------------------------------------------------