Skip to content

Commit

Permalink
Merge pull request react-bootstrap#887 from react-bootstrap/generate-…
Browse files Browse the repository at this point in the history
…prop-info

Generate Prop Documentation
  • Loading branch information
AlexKVal committed Jun 25, 2015
2 parents 67a9166 + 76d62a7 commit 60ddc91
Show file tree
Hide file tree
Showing 12 changed files with 453 additions and 16 deletions.
28 changes: 28 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,34 @@ desired change easier.
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.
Expand Down
7 changes: 7 additions & 0 deletions docs/assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,10 @@ body {
position: absolute;
}

.table .prop-table-row code {
color: #282828;
font-size: 15px;
margin: 0;
padding: 0;
background-color: transparent;
}
11 changes: 7 additions & 4 deletions docs/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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 = '<!doctype html>' + html;
let write = fsp.writeFile(path.join(docsBuilt, fileName), html);
resolve(write);
Expand All @@ -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`),
Expand Down
97 changes: 97 additions & 0 deletions docs/generate-metadata.js
Original file line number Diff line number Diff line change
@@ -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; }));
});
}
29 changes: 19 additions & 10 deletions docs/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(<Handler assetBaseUrl={target} />);
res.send('<!doctype html>' + 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(<Handler assetBaseUrl={target} propData={props}/>);
res.send('<!doctype html>' + html);
});
});
});

} else {
app.use(express.static(path.join(__dirname, '../docs-built')));
}
Expand Down
Loading

0 comments on commit 60ddc91

Please sign in to comment.