From 76d62a76db4df033cf7c41c585d0888e16d3396d Mon Sep 17 00:00:00 2001 From: jquense Date: Tue, 23 Jun 2015 22:36:45 -0400 Subject: [PATCH] Generate Prop Documentation --- CONTRIBUTING.md | 28 +++++++ docs/assets/style.css | 7 ++ docs/build.js | 11 ++- docs/generate-metadata.js | 97 ++++++++++++++++++++++++ docs/server.js | 29 ++++--- docs/src/ComponentsPage.js | 150 ++++++++++++++++++++++++++++++++++++- docs/src/PropTable.js | 103 +++++++++++++++++++++++++ docs/src/Root.js | 10 ++- package.json | 1 + src/BootstrapMixin.js | 12 +++ src/ButtonGroup.js | 4 + tools/promisify.js | 17 +++++ 12 files changed, 453 insertions(+), 16 deletions(-) create mode 100644 docs/generate-metadata.js create mode 100644 docs/src/PropTable.js create mode 100644 tools/promisify.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e6cf80b005..bf156f0a8d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,6 +74,34 @@ check out these [5 useful tips for a better commit message][commit-message] Please update the docs with any API changes, the code and docs should always be in sync. +Component prop documentation is generated automatically from the React components +and their leading comments. Please make sure to provide comments for any `propTypes` you add +or change in a Component. + +```js +propTypes: { + /** + * Sets the visibility of the Component + */ + show: React.PropTypes.bool, + + /** + * A callback fired when the visibility changes + * @type {func} + * @required + */ + onHide: myCustomPropType +} +``` + +There are a few caveats to this format that differ from conventional JSDoc comments. + +- Only specific doclets (the @ things) should be used, and only when the data cannot be parsed from the component itself + - `@type`: Override the "type", use the same names as the default React PropTypes: string, func, bool, number, object. You can express enum and oneOfType types, Like `{("optionA"|"optionB")}`. + - `@required`: to mark a prop as required (use the normal React isRequired if possible) + - `@private`: Will hide the prop in the documentation +- All description text should be above the doclets. + ## Implement additional components and features This project is seeking parity with the core Bootstrap library. diff --git a/docs/assets/style.css b/docs/assets/style.css index 645481e0bf..7f96bbd838 100644 --- a/docs/assets/style.css +++ b/docs/assets/style.css @@ -162,3 +162,10 @@ body { position: absolute; } +.table .prop-table-row code { + color: #282828; + font-size: 15px; + margin: 0; + padding: 0; + background-color: transparent; +} diff --git a/docs/build.js b/docs/build.js index fb50d1c4a7..feccdc030e 100644 --- a/docs/build.js +++ b/docs/build.js @@ -6,6 +6,7 @@ import Root from './src/Root'; import fsp from 'fs-promise'; import { copy } from '../tools/fs-utils'; import { exec } from '../tools/exec'; +import metadata from './generate-metadata'; const repoRoot = path.resolve(__dirname, '../'); const docsBuilt = path.join(repoRoot, 'docs-built'); @@ -21,12 +22,12 @@ const readmeDest = path.join(docsBuilt, 'README.md'); * @return {Promise} promise * @internal */ -function generateHTML(fileName) { +function generateHTML(fileName, propData) { return new Promise((resolve, reject) => { const urlSlug = fileName === 'index.html' ? '/' : `/${fileName}`; Router.run(routes, urlSlug, Handler => { - let html = React.renderToString(React.createElement(Handler)); + let html = React.renderToString(React.createElement(Handler, { propData })); html = '' + html; let write = fsp.writeFile(path.join(docsBuilt, fileName), html); resolve(write); @@ -39,8 +40,10 @@ export default function BuildDocs({ dev }) { return exec(`rimraf ${docsBuilt}`) .then(() => fsp.mkdir(docsBuilt)) - .then(() => { - let pagesGenerators = Root.getPages().map(generateHTML); + .then(metadata) + .then(propData => { + + let pagesGenerators = Root.getPages().map( page => generateHTML(page, propData)); return Promise.all(pagesGenerators.concat([ exec(`webpack --config webpack.docs.js ${dev ? '' : '-p '}--bail`), diff --git a/docs/generate-metadata.js b/docs/generate-metadata.js new file mode 100644 index 0000000000..0df383aee8 --- /dev/null +++ b/docs/generate-metadata.js @@ -0,0 +1,97 @@ +import metadata from 'react-component-metadata'; +import glob from 'glob'; +import fsp from 'fs-promise'; +import promisify from '../tools/promisify'; + +let globp = promisify(glob); + +// removes doclet syntax from comments +let cleanDoclets = desc => { + let idx = desc.indexOf('@'); + return (idx === -1 ? desc : desc.substr(0, idx )).trim(); +}; + +let cleanDocletValue = str => str.replace(/^\{/, '').replace(/\}$/, ''); + +let isLiteral = str => str.trim()[0] === '"' || str.trim()[0] === "'"; + +/** + * parse out description doclets to an object and remove the comment + * + * @param {ComponentMetadata|PropMetadata} obj + */ +function parseDoclets(obj){ + obj.doclets = metadata.parseDoclets(obj.desc || ''); + obj.desc = cleanDoclets(obj.desc || ''); +} + +/** + * Reads the JSDoc "doclets" and applies certain ones to the prop type data + * This allows us to "fix" parsing errors, or unparsable data with JSDoc style comments + * + * @param {Object} props Object Hash of the prop metadata + * @param {String} propName + */ +function applyPropDoclets(props, propName){ + let prop = props[propName]; + let doclets = prop.doclets; + let value; + + // the @type doclet to provide a prop type + // Also allows enums (oneOf) if string literals are provided + // ex: @type {("optionA"|"optionB")} + if (doclets.type) { + value = cleanDocletValue(doclets.type); + prop.type.name = value; + + if ( value[0] === '(' ) { + value = value.substring(1, value.length - 1).split('|'); + + prop.type.value = value; + prop.type.name = value.every(isLiteral) ? 'enum' : 'union'; + } + } + + // Use @required to mark a prop as required + // useful for custom propTypes where there isn't a `.isRequired` addon + if ( doclets.required) { + prop.required = true; + } +} + + +export default function generate(destination, options = { mixins: true }){ + + return globp(__dirname + '/../src/**/*.js') //eslint-disable-line no-path-concat + .then( files => { + + let results = files.map( + filename => fsp.readFile(filename).then(content => metadata(content, options)) ); + + return Promise.all(results) + .then( data => { + let result = {}; + + data.forEach(components => { + Object.keys(components).forEach(key => { + const component = components[key]; + + parseDoclets(component); + + Object.keys(component.props).forEach( propName => { + const prop = component.props[propName]; + + parseDoclets(prop); + applyPropDoclets(component.props, propName); + }); + }); + + //combine all the component metadata into one large object + result = { ...result, ...components }; + }); + + return result; + }) + .catch( e => setTimeout(()=> { throw e; })); + }); +} diff --git a/docs/server.js b/docs/server.js index 5d0157dfca..66b49144c8 100644 --- a/docs/server.js +++ b/docs/server.js @@ -5,6 +5,8 @@ import path from 'path'; import Router from 'react-router'; import routes from './src/Routes'; import httpProxy from 'http-proxy'; + +import metadata from './generate-metadata'; import ip from 'ip'; const development = process.env.NODE_ENV !== 'production'; @@ -21,20 +23,27 @@ if (development) { proxy.web(req, res, { target }); }); - app.use(function renderApp(req, res) { - res.header('Access-Control-Allow-Origin', target); - res.header('Access-Control-Allow-Headers', 'X-Requested-With'); - - Router.run(routes, req.url, Handler => { - let html = React.renderToString(); - res.send('' + html); - }); - }); - proxy.on('error', function(e) { console.log('Could not connect to webpack proxy'.red); console.log(e.toString().red); }); + + console.log('Prop data generation started:'.green); + + metadata().then( props => { + console.log('Prop data generation finished:'.green); + + app.use(function renderApp(req, res) { + res.header('Access-Control-Allow-Origin', target); + res.header('Access-Control-Allow-Headers', 'X-Requested-With'); + + Router.run(routes, req.url, Handler => { + let html = React.renderToString(); + res.send('' + html); + }); + }); + }); + } else { app.use(express.static(path.join(__dirname, '../docs-built'))); } diff --git a/docs/src/ComponentsPage.js b/docs/src/ComponentsPage.js index 93aa06266e..95f5d3732f 100644 --- a/docs/src/ComponentsPage.js +++ b/docs/src/ComponentsPage.js @@ -9,6 +9,7 @@ import NavItem from '../../src/NavItem'; import NavMain from './NavMain'; import PageHeader from './PageHeader'; +import PropTable from './PropTable'; import PageFooter from './PageFooter'; import ReactPlayground from './ReactPlayground'; import Samples from './Samples'; @@ -67,6 +68,7 @@ const ComponentsPage = React.createClass({ flush against each other. To preserve the spacing between multiple inline buttons, wrap your button group in {''}.

+

Sizes

Fancy larger or smaller buttons? Add bsSize="large", bsSize="small", or bsSize="xsmall" for additional sizes.

@@ -99,6 +101,10 @@ const ComponentsPage = React.createClass({ feedback as to the loading state, this can easily be done by updating your {'