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 {''}’s props from a state change like below.
+
+
Add responsive prop to make them scroll horizontally up to small devices (under 768px). When viewing on anything larger than 768px wide, you will not see any difference in these tables.