Adding react-docgen for documentation generation

Usage: cd website/react-docgen && npm install
or: cd website/react-docgen && npm install -g ./
This commit is contained in:
Felix Kling 2015-02-12 11:51:52 -08:00
parent bd53b4191f
commit ba7021f6f8
28 changed files with 2557 additions and 0 deletions

View File

@ -0,0 +1,30 @@
BSD License
For React docs generator software
Copyright (c) 2015, Facebook, Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name Facebook nor the names of its contributors may be used to
endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,23 @@
Additional Grant of Patent Rights
"Software" means the React docs generator software distributed by Facebook, Inc.
Facebook hereby grants you a perpetual, worldwide, royalty-free, non-exclusive,
irrevocable (subject to the termination provision below) license under any
rights in any patent claims owned by Facebook, to make, have made, use, sell,
offer to sell, import, and otherwise transfer the Software. For avoidance of
doubt, no license is granted under Facebooks rights in any patent claims that
are infringed by (i) modifications to the Software made by you or a third party,
or (ii) the Software in combination with any software or other technology
provided by you or a third party.
The license granted hereunder will terminate, automatically and without notice,
for anyone that makes any claim (including by filing any lawsuit, assertion or
other action) alleging (a) direct, indirect, or contributory infringement or
inducement to infringe any patent: (i) by Facebook or any of its subsidiaries or
affiliates, whether or not such claim is related to the Software, (ii) by any
party if such claim arises in whole or in part from any software, product or
service of Facebook or any of its subsidiaries or affiliates, whether or not
such claim is related to the Software, or (iii) by any party relating to the
Software; or (b) that any right in any patent claim of Facebook is invalid or
unenforceable.

View File

@ -0,0 +1,175 @@
# react-docs-generator
`react-docs-generator` extracts information from React components with which
you can generate documentation for those components.
It uses esprima-fb to parse the provided files into an AST, looks for React
component definitions, and inspects the `propTypes` and `getDefaultProps`
declarations. The output is a JSON blob with the extracted information.
Note that component definitions must follow certain guidelines in order to be
analyzable by this tool. We will work towards less strict guidelines, but there
is a limit to what is statically analyzable.
## Install
Install the module directly from npm:
```
npm install -g react-docs-generator
```
## CLI
Installing the module adds a `react-docs` executable which allows you do convert
a single file, multiple files or an input stream. We are trying to make the
executable as versatile as possible so that it can be integrated into many
workflows.
```
Usage: react-docs [path]... [options]
path A component file or directory. If no path is provided it reads from stdin.
Options:
-o FILE, --out FILE store extracted information in FILE
--pretty pretty print JSON
-x, --extension File extensions to consider. Repeat to define multiple extensions. Default: [js,jsx]
-i, --ignore Folders to ignore. Default: [node_modules,__tests__]
Extract meta information from React components.
If a directory is passed, it is recursively traversed.
```
## API
The tool can also be used programmatically to extract component information:
```js
var reactDocs = require('react-docs-generator');
var componentInfo reactDocs.parseSource(src);
```
## Guidelines
- Modules have to export a single component, and only that component is
analyzed.
- `propTypes` must be an object literal or resolve to an object literal in the
same file.
- The `return` statement in `getDefaultProps` must consist of an object literal.
## Example
For the following component
```js
var React = require('react');
/**
* General component description.
*/
var Component = React.createClass({
propTypes: {
/**
* Description of prop "foo".
*/
foo: React.PropTypes.number,
/**
* Description of prop "bar" (a custom validation function).
*/
bar: function(props, propName, componentName) {
// ...
},
baz: React.PropTypes.oneOfType([
React.PropTypes.number,
React.PropTypes.string
]),
},
getDefaultProps: function() {
return {
foo: 42,
bar: 21
};
},
render: function() {
// ...
}
});
module.exports = Component;
```
we are getting this output:
```
{
"props": {
"foo": {
"type": {
"name": "number"
},
"required": false,
"description": "Description of prop \"foo\".",
"defaultValue": {
"value": "42",
"computed": false
}
},
"bar": {
"type": {
"name": "custom"
},
"required": false,
"description": "Description of prop \"bar\" (a custom validation function).",
"defaultValue": {
"value": "21",
"computed": false
}
},
"baz": {
"type": {
"name": "union",
"value": [
{
"name": "number"
},
{
"name": "string"
}
]
},
"required": false,
"description": ""
}
},
"description": "General component description."
}
```
## Result data structure
The structure of the JSON blob / JavaScript object is as follows:
```
{
"description": string
"props": {
"<propName>": {
"type": {
"name": "<typeName>",
["value": <typeValue>]
},
"required": boolean,
"description": string,
["defaultValue": {
"value": number | string,
"computed": boolean
}]
},
...
},
["composes": <componentNames>]
}
```

168
website/react-docgen/bin/react-docgen.js vendored Executable file
View File

@ -0,0 +1,168 @@
#!/usr/bin/env node
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
var argv = require('nomnom')
.script('react-docs')
.help(
'Extract meta information from React components.\n' +
'If a directory is passed, it is recursively traversed.'
)
.options({
path: {
position: 0,
help: 'A component file or directory. If no path is provided it reads from stdin.',
metavar: 'PATH',
list: true
},
out: {
abbr: 'o',
help: 'store extracted information in FILE',
metavar: 'FILE'
},
pretty: {
help: 'pretty print JSON',
flag: true
},
extension: {
abbr: 'x',
help: 'File extensions to consider. Repeat to define multiple extensions. Default:',
list: true,
default: ['js', 'jsx']
},
ignoreDir: {
abbr: 'i',
full: 'ignore',
help: 'Folders to ignore. Default:',
list: true,
default: ['node_modules', '__tests__']
}
})
.parse();
var async = require('async');
var dir = require('node-dir');
var fs = require('fs');
var parser = require('../dist/main.js');
var output = argv.o;
var paths = argv.path;
var extensions = new RegExp('\\.(?:' + argv.extension.join('|') + ')$');
var ignoreDir = argv.ignoreDir;
function writeError(msg, path) {
if (path) {
process.stderr.write('Error with path "' + path + '": ');
}
process.stderr.write(msg + '\n');
}
function exitWithError(error) {
writeError(error);
process.exit(1);
}
function exitWithResult(result) {
result = argv.pretty ?
JSON.stringify(result, null, 2) :
JSON.stringify(result);
if (argv.o) {
fs.writeFileSync(argv.o, result);
} else {
process.stdout.write(result + '\n');
}
process.exit(0);
}
/**
* 1. No files passed, consume input stream
*/
if (paths.length === 0) {
var source = '';
process.stdin.setEncoding('utf8');
process.stdin.resume();
var timer = setTimeout(function() {
process.stderr.write('Still waiting for std input...');
}, 5000);
process.stdin.on('data', function (chunk) {
clearTimeout(timer);
source += chunk;
});
process.stdin.on('end', function () {
exitWithResult(parser.parseSource(source));
});
}
function traverseDir(path, result, done) {
dir.readFiles(
path,
{
match: extensions,
excludeDir: ignoreDir
},
function(error, content, filename, next) {
if (error) {
exitWithError(error);
}
try {
result[filename] = parser.parseSource(content);
} catch(error) {
writeError(error, path);
}
next();
},
function(error) {
if (error) {
writeError(error);
}
done();
}
);
}
/**
* 2. Paths are passed.
*/
var result = Object.create(null);
async.eachSeries(paths, function(path, done) {
fs.stat(path, function(error, stats) {
if (error) {
writeError(error, path);
done();
return;
}
if (stats.isDirectory()) {
traverseDir(path, result, done);
}
else {
try {
result[path] = parser.parseSource(fs.readFileSync(path));
} catch(error) {
writeError(error, path);
}
finally {
done();
}
}
});
}, function() {
var resultsPaths = Object.keys(result);
if (resultsPaths.length === 0) {
// we must have gotten an error
process.exit(1);
}
if (paths.length === 1) { // a single path?
fs.stat(paths[0], function(error, stats) {
exitWithResult(stats.isDirectory() ? result : result[resultsPaths[0]]);
});
} else {
exitWithResult(result);
}
});

30
website/react-docgen/flow/recast.js vendored Normal file
View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* A minimal set of declarations to make flow work with the recast API.
*/
type ASTNode = Object;
declare class Scope {
lookup(name: string): ?Scope;
getBindings(): Object<string, Array<NodePath>>;
}
declare class NodePath {
node: ASTNode;
parent: NodePath;
scope: Scope;
get(...x: (string|number)): NodePath;
each(f: (p: NodePath) => void): void;
map<T>(f: (p: NodePath) => T): Array<T>;
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* @flow
*/
"use strict";
type PropDescriptor = {
type?: {
name: string;
value?: any;
};
required?: boolean;
defaultValue?: any;
description?: string;
};
class Documentation {
_props: Object;
_description: string;
_composes: Array<string>;
constructor() {
this._props = {};
this._description = '';
this._composes = [];
}
addComposes(moduleName: string) {
if (this._composes.indexOf(moduleName) === -1) {
this._composes.push(moduleName);
}
}
getDescription(): string {
return this._description;
}
setDescription(description: string): void {
this._description = description;
}
getPropDescriptor(propName: string): PropDescriptor {
var propDescriptor = this._props[propName];
if (!propDescriptor) {
propDescriptor = this._props[propName] = {};
}
return propDescriptor;
}
toObject(): Object {
var obj = {
description: this._description,
props: this._props
};
if (this._composes.length) {
obj.composes = this._composes;
}
return obj;
}
}
module.exports = Documentation;

View File

@ -0,0 +1,213 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* @flow
*/
"use strict";
/**
* How this parser works:
*
* 1. For each given file path do:
*
* a. Find component definition
* -. Find the rvalue module.exports assignment.
* Otherwise inspect assignments to exports. If there are multiple
* components that are exported, we don't continue with parsing the file.
* -. If the previous step results in a variable name, resolve it.
* -. Extract the object literal from the React.createClass call.
*
* b. Execute definition handlers (handlers working with the object
* expression).
*
* c. For each property of the definition object, execute the registered
* callbacks, if they are eligible for this property.
*
* 2. Return the aggregated results
*/
type Handler = (documentation: Documentation, path: NodePath) => void;
var Documentation = require('./Documentation');
var expressionTo = require('./utils/expressionTo');
var getPropertyName = require('./utils/getPropertyName');
var isReactModuleName = require('./utils/isReactModuleName');
var match = require('./utils/match');
var resolveToValue = require('./utils/resolveToValue');
var resolveToModule = require('./utils/resolveToModule');
var recast = require('recast');
var n = recast.types.namedTypes;
function ignore() {
return false;
}
/**
* Returns true if the statement is of form `foo = bar;`.
*
* @param {object} node
* @return {bool}
*/
function isAssignmentStatement(node) {
return match(node, {expression: {operator: '='}});
}
/**
* Returns true if the expression is of form `exports.foo = bar;` or
* `modules.exports = foo;`.
*
* @param {object} node
* @return {bool}
*/
function isExportsOrModuleExpression(path) {
if (!n.AssignmentExpression.check(path.node) ||
!n.MemberExpression.check(path.node.left)) {
return false;
}
var exprArr = expressionTo.Array(path.get('left'));
return (exprArr[0] === 'module' && exprArr[1] === 'exports') ||
exprArr[0] == 'exports';
}
/**
* Returns true if the expression is a function call of the form
* `React.createClass(...)`.
*
* @param {object} node
* @param {array} scopeChain
* @return {bool}
*/
function isReactCreateClassCall(path) {
if (!match(path.node, {callee: {property: {name: 'createClass'}}})) {
return false;
}
var module = resolveToModule(path.get('callee', 'object'));
return module && isReactModuleName(module);
}
/**
* Given an AST, this function tries to find the object expression that is
* passed to `React.createClass`, by resolving all references properly.
*
* @param {object} ast
* @return {?object}
*/
function findComponentDefinition(ast) {
var definition;
recast.visit(ast, {
visitFunctionDeclaration: ignore,
visitFunctionExpression: ignore,
visitIfStatement: ignore,
visitWithStatement: ignore,
visitSwitchStatement: ignore,
visitTryStatement: ignore,
visitWhileStatement: ignore,
visitDoWhileStatement: ignore,
visitForStatement: ignore,
visitForInStatement: ignore,
visitAssignmentExpression: function(path) {
// Ignore anything that is not `exports.X = ...;` or
// `module.exports = ...;`
if (!isExportsOrModuleExpression(path)) {
return false;
}
// Resolve the value of the right hand side. It should resolve to a call
// expression, something like React.createClass
path = resolveToValue(path.get('right'));
if (!isReactCreateClassCall(path)) {
return false;
}
if (definition) { // If a file exports multiple components, ... complain!
throw new Error(ReactDocumentationParser.ERROR_MULTIPLE_DEFINITIONS);
}
// We found React.createClass. Lets get cracking!
definition = resolveToValue(path.get('arguments', 0));
return false;
}
});
return definition;
}
class ReactDocumentationParser {
_componentHandlers: Array<Handler>;
_propertyHandlers: Object<string, Handler>;
constructor() {
this._componentHandlers = [];
this._propertyHandlers = Object.create(null);
}
/**
* Handlers extract information from the component definition.
*
* If "property" is not provided, the handler is passed the whole component
* definition.
*/
addHandler(handler: Handler, property?: string): void {
if (!property) {
this._componentHandlers.push(handler);
} else {
if (!this._propertyHandlers[property]) {
this._propertyHandlers[property] = [];
}
this._propertyHandlers[property].push(handler);
}
}
/**
* Takes JavaScript source code and returns an object with the information
* extract from it.
*/
parseSource(source: string): Object {
var documentation = new Documentation();
var ast = recast.parse(source);
// Find the component definition first. The return value should be
// an ObjectExpression.
var componentDefinition = findComponentDefinition(ast.program);
if (!componentDefinition) {
throw new Error(ReactDocumentationParser.ERROR_MISSING_DEFINITION);
}
// Execute all the handlers to extract the information
this._executeHandlers(documentation, componentDefinition);
return documentation.toObject();
}
_executeHandlers(documentation, componentDefinition: NodePath) {
componentDefinition.get('properties').each(propertyPath => {
var name = getPropertyName(propertyPath);
if (!this._propertyHandlers[name]) {
return;
}
var propertyValuePath = propertyPath.get('value');
this._propertyHandlers[name].forEach(
handler => handler(documentation, propertyValuePath)
);
});
this._componentHandlers.forEach(
handler => handler(documentation, componentDefinition)
);
}
}
ReactDocumentationParser.ERROR_MISSING_DEFINITION =
'No suitable component definition found.';
ReactDocumentationParser.ERROR_MULTIPLE_DEFINITIONS =
'Multiple exported component definitions found.';
module.exports = ReactDocumentationParser;

View File

@ -0,0 +1,118 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
"use strict";
require('mock-modules').autoMockOff();
describe('React documentation parser', function() {
var ReactDocumentationParser;
var parser;
beforeEach(function() {
ReactDocumentationParser = require('../ReactDocumentationParser');
parser = new ReactDocumentationParser();
});
it('errors if component definition is not found', function() {
var source = 'var React = require("React");';
expect(function() {
parser.parseSource(source);
}).toThrow(ReactDocumentationParser.ERROR_MISSING_DEFINITION);
});
it('finds React.createClass', function() {
var source = [
'var React = require("React");',
'var Component = React.createClass({});',
'module.exports = Component;'
].join('\n');
expect(function() {
parser.parseSource(source);
}).not.toThrow();
});
it('finds React.createClass, independent of the var name', function() {
var source = [
'var R = require("React");',
'var Component = R.createClass({});',
'module.exports = Component;'
].join('\n');
expect(function() {
parser.parseSource(source);
}).not.toThrow();
});
it('does not process X.createClass of other modules', function() {
var source = [
'var R = require("NoReact");',
'var Component = R.createClass({});',
'module.exports = Component;'
].join('\n');
expect(function() {
parser.parseSource(source);
}).toThrow(ReactDocumentationParser.ERROR_MISSING_DEFINITION);
});
it('finds assignments to exports', function() {
var source = [
'var R = require("React");',
'var Component = R.createClass({});',
'exports.foo = 42;',
'exports.Component = Component;'
].join('\n');
expect(function() {
parser.parseSource(source);
}).not.toThrow();
});
it('errors if multiple components are exported', function() {
var source = [
'var R = require("React");',
'var ComponentA = R.createClass({});',
'var ComponentB = R.createClass({});',
'exports.ComponentA = ComponentA;',
'exports.ComponentB = ComponentB;'
].join('\n');
expect(function() {
parser.parseSource(source);
}).toThrow(ReactDocumentationParser.ERROR_MULTIPLE_DEFINITIONS);
});
it('accepts multiple definitions if only one is exported', function() {
var source = [
'var R = require("React");',
'var ComponentA = R.createClass({});',
'var ComponentB = R.createClass({});',
'exports.ComponentB = ComponentB;'
].join('\n');
expect(function() {
parser.parseSource(source);
}).not.toThrow();
source = [
'var R = require("React");',
'var ComponentA = R.createClass({});',
'var ComponentB = R.createClass({});',
'module.exports = ComponentB;'
].join('\n');
expect(function() {
parser.parseSource(source);
}).not.toThrow();
});
});

View File

@ -0,0 +1,96 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
"use strict";
jest.autoMockOff();
describe('React documentation parser', function() {
var parser;
beforeEach(function() {
parser = new (require('../../ReactDocumentationParser'));
parser.addHandler(require('../componentDocblockHandler'));
});
it('finds docblocks for component definitions', function() {
var source = [
'var React = require("React");',
'/**',
' * Component description',
' */',
'var Component = React.createClass({});',
'module.exports = Component;'
].join('\n');
var expectedResult = {
props: {},
description: 'Component description'
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
it('ignores other types of comments', function() {
var source = [
'var React = require("React");',
'/*',
' * This is not a docblock',
' */',
'var Component = React.createClass({});',
'module.exports = Component;'
].join('\n');
var expectedResult = {
props: {},
description: ''
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
source = [
'var React = require("React");',
'// Inline comment',
'var Component = React.createClass({});',
'module.exports = Component;'
].join('\n');
expectedResult = {
props: {},
description: ''
};
result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
it('only considers the docblock directly above the definition', function() {
var source = [
'var React = require("React");',
'/**',
' * This is the wrong docblock',
' */',
'var something_else = "foo";',
'var Component = React.createClass({});',
'module.exports = Component;'
].join('\n');
var expectedResult = {
props: {},
description: ''
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
});

View File

@ -0,0 +1,81 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
"use strict";
jest.autoMockOff();
var module_template = [
'var React = require("React");',
'var PropTypes = React.PropTypes;',
'var Component = React.createClass(%s);',
'module.exports = Component;'
].join('\n');
function getSource(definition) {
return module_template.replace('%s', definition);
}
describe('React documentation parser', function() {
var parser;
beforeEach(function() {
parser = new (require('../../ReactDocumentationParser'));
parser.addHandler(require('../defaultValueHandler'), 'getDefaultProps');
});
it ('should find prop default values that are literals', function() {
var source = getSource([
'{',
' getDefaultProps: function() {',
' return {',
' foo: "bar",',
' bar: 42,',
' baz: ["foo", "bar"],',
' abc: {xyz: abc.def, 123: 42}',
' };',
' }',
'}'
].join('\n'));
var expectedResult = {
description: '',
props: {
foo: {
defaultValue: {
value: '"bar"',
computed: false
}
},
bar: {
defaultValue: {
value: '42',
computed: false
}
},
baz: {
defaultValue: {
value: '["foo", "bar"]',
computed: false
}
},
abc: {
defaultValue: {
value: '{xyz: abc.def, 123: 42}',
computed: false
}
}
}
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
});

View File

@ -0,0 +1,189 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
"use strict";
jest.autoMockOff();
var module_template = [
'var React = require("React");',
'var PropTypes = React.PropTypes;',
'var Component = React.createClass(%s);',
'module.exports = Component;'
].join('\n');
function getSource(definition) {
return module_template.replace('%s', definition);
}
describe('React documentation parser', function() {
var parser;
beforeEach(function() {
parser = new (require('../../ReactDocumentationParser'));
parser.addHandler(require('../propDocblockHandler'), 'propTypes');
});
it('finds docblocks for prop types', function() {
var source = getSource([
'{',
' propTypes: {',
' /**',
' * Foo comment',
' */',
' foo: Prop.bool,',
'',
' /**',
' * Bar comment',
' */',
' bar: Prop.bool,',
' }',
'}'
].join('\n'));
var expectedResult = {
description: '',
props: {
foo: {
description: 'Foo comment'
},
bar: {
description: 'Bar comment'
}
}
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
it('can handle multline comments', function() {
var source = getSource([
'{',
' propTypes: {',
' /**',
' * Foo comment with',
' * many lines!',
' *',
' * even with empty lines in between',
' */',
' foo: Prop.bool',
' }',
'}'
].join('\n'));
var expectedResult = {
description: '',
props: {
foo: {
description:
'Foo comment with\nmany lines!\n\neven with empty lines in between'
}
}
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
it('ignores non-docblock comments', function() {
var source = getSource([
'{',
' propTypes: {',
' /**',
' * Foo comment',
' */',
' // TODO: remove this comment',
' foo: Prop.bool,',
'',
' /**',
' * Bar comment',
' */',
' /* This is not a doc comment */',
' bar: Prop.bool,',
' }',
'}'
].join('\n'));
var expectedResult = {
description: '',
props: {
foo: {
description: 'Foo comment'
},
bar: {
description: 'Bar comment'
}
}
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
it('only considers the comment with the property below it', function() {
var source = getSource([
'{',
' propTypes: {',
' /**',
' * Foo comment',
' */',
' foo: Prop.bool,',
' bar: Prop.bool,',
' }',
'}'
].join('\n'));
var expectedResult = {
description: '',
props: {
foo: {
description: 'Foo comment'
},
bar: {
description: ''
}
}
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
it('understands and ignores the spread operator', function() {
var source = getSource([
'{',
' propTypes: {',
' ...Foo.propTypes,',
' /**',
' * Foo comment',
' */',
' foo: Prop.bool,',
' bar: Prop.bool,',
' }',
'}'
].join('\n'));
var expectedResult = {
description: '',
props: {
foo: {
description: 'Foo comment'
},
bar: {
description: ''
}
}
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
});

View File

@ -0,0 +1,456 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
"use strict";
jest.autoMockOff();
var module_template = [
'var React = require("React");',
'var PropTypes = React.PropTypes;',
'var Component = React.createClass(%s);',
'module.exports = Component;'
].join('\n');
function getSource(definition) {
return module_template.replace('%s', definition);
}
describe('React documentation parser', function() {
var parser;
beforeEach(function() {
parser = new (require('../../ReactDocumentationParser'));
parser.addHandler(require('../propTypeHandler'), 'propTypes');
});
it('finds definitions via React.PropTypes', function() {
var source = [
'var React = require("React");',
'var Prop = React.PropTypes;',
'var Prop1 = require("React").PropTypes;',
'var Component = React.createClass({',
' propTypes: {',
' foo: Prop.bool,',
' bar: Prop1.bool,',
' }',
'});',
'module.exports = Component;'
].join('\n');
var expectedResult = {
description: '',
props: {
foo: {
type: {name: 'bool'},
required: false
},
bar: {
type: {name: 'bool'},
required: false
}
}
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
it('finds definitions via the ReactPropTypes module', function() {
var source = [
'var React = require("React");',
'var Prop = require("ReactPropTypes");',
'var Component = React.createClass({',
' propTypes: {',
' foo: Prop.bool,',
' }',
'});',
'module.exports = Component;'
].join('\n');
var expectedResult = {
description: '',
props: {
foo: {
type: {name: 'bool'},
required: false
}
}
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
it('detects simple prop types', function() {
var source = getSource([
'{',
' propTypes: {',
' array_prop: PropTypes.array,',
' bool_prop: PropTypes.bool,',
' func_prop: PropTypes.func,',
' number_prop: PropTypes.number,',
' object_prop: PropTypes.object,',
' string_prop: PropTypes.string,',
' element_prop: PropTypes.element,',
' any_prop: PropTypes.any,',
' node_prop: PropTypes.node',
' }',
'}'
].join('\n'));
var expectedResult = {
description: '',
props:{
array_prop: {
type: {name: 'array'},
required: false
},
bool_prop: {
type: {name: 'bool'},
required: false
},
func_prop: {
type: {name: 'func'},
required: false
},
number_prop: {
type: {name: 'number'},
required: false
},
object_prop: {
type: {name: 'object'},
required: false
},
string_prop: {
type: {name: 'string'},
required: false
},
element_prop: {
type: {name: 'element'},
required: false
},
any_prop: {
type: {name: 'any'},
required: false
},
node_prop: {
type: {name: 'node'},
required: false
}
}
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
it.only('detects complex prop types', function() {
var source = getSource([
'{',
' propTypes: {',
' oneOf_prop: PropTypes.oneOf(["foo", "bar"]),',
' oneOfType_prop:',
' PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),',
' oneOfType_custom_prop:',
' PropTypes.oneOfType([xyz]),',
' instanceOf_prop: PropTypes.instanceOf(Foo),',
' arrayOf_prop: PropTypes.arrayOf(PropTypes.string),',
' shape_prop:',
' PropTypes.shape({foo: PropTypes.string, bar: PropTypes.bool}),',
' shape_custom_prop:',
' PropTypes.shape({foo: xyz})',
' }',
'}'
].join('\n'));
var expectedResult = {
description: '',
props:{
oneOf_prop: {
type: {
name: 'enum',
value: [
{value: '"foo"', computed: false},
{value: '"bar"', computed: false}
]
},
required: false
},
oneOfType_prop: {
type: {
name:'union',
value: [
{name: 'number'},
{name: 'bool'}
]
},
required: false
},
oneOfType_custom_prop: {
type: {
name:'union',
value: [
{name: 'custom'}
]
},
required: false
},
instanceOf_prop: {
type: {
name: 'instance',
value: 'Foo'
},
required: false
},
arrayOf_prop: {
type: {
name: 'arrayof',
value: {name: 'string'}
},
required: false
},
shape_prop: {
type: {
name: 'shape',
value: {
foo: {name: 'string'},
bar: {name: 'bool'}
}
},
required: false
},
shape_custom_prop: {
type: {
name: 'shape',
value: {
foo: {name: 'custom'},
}
},
required: false
}
}
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
it('resolves variables to their values', function() {
var source = [
'var React = require("React");',
'var PropTypes = React.PropTypes;',
'var shape = {bar: PropTypes.string};',
'var Component = React.createClass({',
' propTypes: {',
' foo: PropTypes.shape(shape)',
' }',
'});',
'module.exports = Component;'
].join('\n');
var expectedResult = {
description: '',
props: {
foo: {
type: {
name: 'shape',
value: {
bar: {name: 'string'}
}
},
required: false
}
}
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
it('detects whether a prop is required', function() {
var source = getSource([
'{',
' propTypes: {',
' array_prop: PropTypes.array.isRequired,',
' bool_prop: PropTypes.bool.isRequired,',
' func_prop: PropTypes.func.isRequired,',
' number_prop: PropTypes.number.isRequired,',
' object_prop: PropTypes.object.isRequired,',
' string_prop: PropTypes.string.isRequired,',
' element_prop: PropTypes.element.isRequired,',
' any_prop: PropTypes.any.isRequired,',
' oneOf_prop: PropTypes.oneOf(["foo", "bar"]).isRequired,',
' oneOfType_prop: ',
' PropTypes.oneOfType([PropTypes.number, PropTypes.bool]).isRequired,',
' instanceOf_prop: PropTypes.instanceOf(Foo).isRequired',
' }',
'}'
].join('\n'));
var expectedResult = {
description: '',
props:{
array_prop: {
type: {name: 'array'},
required: true
},
bool_prop: {
type: {name: 'bool'},
required: true
},
func_prop: {
type: {name: 'func'},
required: true
},
number_prop: {
type: {name: 'number'},
required: true
},
object_prop: {
type: {name: 'object'},
required: true
},
string_prop: {
type: {name: 'string'},
required: true
},
element_prop: {
type: {name: 'element'},
required: true
},
any_prop: {
type: {name: 'any'},
required: true
},
oneOf_prop: {
type: {
name: 'enum',
value: [
{value: '"foo"', computed: false},
{value: '"bar"', computed: false}
]
},
required: true
},
oneOfType_prop: {
type: {
name: 'union',
value: [
{name: 'number'},
{name: 'bool'}
]
},
required: true
},
instanceOf_prop: {
type: {
name: 'instance',
value: 'Foo'
},
required: true
}
}
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
it('detects custom validation functions', function() {
var source = getSource([
'{',
' propTypes: {',
' custom_prop: function() {}',
' }',
'}'
].join('\n'));
var expectedResult = {
description: '',
props: {
custom_prop: {
type: {name: 'custom'},
required: false
}
}
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
it('only considers definitions from React or ReactPropTypes', function() {
var source = [
'var React = require("React");',
'var PropTypes = React.PropTypes;',
'var Prop = require("Foo");',
'var Component = React.createClass({',
' propTypes: {',
' custom_propA: PropTypes.bool,',
' custom_propB: Prop.bool.isRequired',
' }',
'});',
'module.exports = Component;'
].join('\n');
var expectedResult = {
description: '',
props: {
custom_propA: {
type: {name: 'bool'},
required: false
},
custom_propB: {
type: {name: 'custom'},
required: false
}
}
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
it('understands the spread operator', function() {
var source = [
'var React = require("React");',
'var PropTypes = React.PropTypes;',
'var Foo = require("Foo.react");',
'var props = {bar: PropTypes.bool};',
'var Component = React.createClass({',
' propTypes: {',
' ...Foo.propTypes,',
' ...props,',
' foo: PropTypes.number',
' }',
'});',
'module.exports = Component;'
].join('\n');
var expectedResult = {
description: '',
composes: ['Foo.react'],
props:{
foo: {
type: {name: 'number'},
required: false
},
bar: {
type: {name: 'bool'},
required: false
},
}
};
var result = parser.parseSource(source);
expect(result).toEqual(expectedResult);
});
});

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* @flow
*/
"use strict";
var Documentation = require('../Documentation');
var n = require('recast').types.namedTypes;
var getDocblock = require('../utils/docblock').getDocblock;
/**
* Finds the nearest block comment before the component definition.
*/
function componentDocblockHandler(
documentation: Documentation,
path: NodePath
) {
var description = '';
// Find parent statement (e.g. var Component = React.createClass(path);)
while (path && !n.Statement.check(path.node)) {
path = path.parent;
}
if (path) {
description = getDocblock(path) || '';
}
documentation.setDescription(description);
}
module.exports = componentDocblockHandler;

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* @flow
*/
"use strict";
var Documentation = require('../Documentation');
var expressionTo = require('../utils/expressionTo');
var getPropertyName = require('../utils/getPropertyName');
var recast = require('recast');
var resolveToValue = require('../utils/resolveToValue');
var types = recast.types.namedTypes;
var visit = recast.types.visit;
function getDefaultValue(path) {
var node = path.node;
var defaultValue;
if (types.Literal.check(node)) {
defaultValue = node.raw;
} else {
path = resolveToValue(path);
node = path.node;
defaultValue = expressionTo.String(path);
}
if (typeof defaultValue !== 'undefined') {
return {
value: defaultValue,
computed: types.CallExpression.check(node) ||
types.MemberExpression.check(node) ||
types.Identifier.check(node)
};
}
}
function defaultValueHandler(documentation: Documentation, path: NodePath) {
if (!types.FunctionExpression.check(path.node)) {
return;
}
// Find the value that is returned from the function and process it if it is
// an object literal.
var objectExpressionPath;
visit(path.get('body'), {
visitFunction: () => false,
visitReturnStatement: function(path) {
var resolvedPath = resolveToValue(path.get('argument'));
if (types.ObjectExpression.check(resolvedPath.node)) {
objectExpressionPath = resolvedPath;
}
return false;
}
});
if (objectExpressionPath) {
objectExpressionPath.get('properties').each(function(propertyPath) {
var propDescriptor = documentation.getPropDescriptor(
getPropertyName(propertyPath)
);
var defaultValue = getDefaultValue(propertyPath.get('value'));
if (defaultValue) {
propDescriptor.defaultValue = defaultValue;
}
});
}
}
module.exports = defaultValueHandler;

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* @flow
*/
"use strict";
var Documentation = require('../Documentation');
var types = require('recast').types.namedTypes;
var getDocblock = require('../utils/docblock').getDocblock;
var getPropertyName = require('../utils/getPropertyName');
function propDocBlockHandler(documentation: Documentation, path: NodePath) {
if (!types.ObjectExpression.check(path.node)) {
return;
}
path.get('properties').each(function(propertyPath) {
// we only support documentation of actual properties, not spread
if (types.Property.check(propertyPath.node)) {
var propDescriptor = documentation.getPropDescriptor(
getPropertyName(propertyPath)
);
propDescriptor.description = getDocblock(propertyPath) || '';
}
});
}
module.exports = propDocBlockHandler;

View File

@ -0,0 +1,255 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* @flow
*/
"use strict";
var Documentation = require('../Documentation');
var expressionTo = require('../utils/expressionTo');
var getNameOrValue = require('../utils/getNameOrValue');
var getPropertyName = require('../utils/getPropertyName');
var isReactModuleName = require('../utils/isReactModuleName');
var resolveToModule = require('../utils/resolveToModule');
var resolveToValue = require('../utils/resolveToValue');
var types = require('recast').types.namedTypes;
var simplePropTypes = {
array: 1,
bool: 1,
func: 1,
number: 1,
object: 1,
string: 1,
any: 1,
element: 1,
node: 1
};
function isPropTypesExpression(path) {
var moduleName = resolveToModule(path);
if (moduleName) {
return isReactModuleName(moduleName) || moduleName === 'ReactPropTypes';
}
return false;
}
function getEnumValues(path) {
return path.get('elements').map(function(elementPath) {
return {
value: expressionTo.String(elementPath),
computed: !types.Literal.check(elementPath.node)
};
});
}
function getPropTypeOneOf(path) {
types.CallExpression.assert(path.node);
var argumentPath = path.get('arguments', 0);
var type = {name: 'enum'};
if (!types.ArrayExpression.check(argumentPath.node)) {
type.computed = true;
type.value = expressionTo.String(argumentPath);
} else {
type.value = getEnumValues(argumentPath);
}
return type;
}
function getPropTypeOneOfType(path) {
types.CallExpression.assert(path.node);
var argumentPath = path.get('arguments', 0);
var type = {name: 'union'};
if (!types.ArrayExpression.check(argumentPath.node)) {
type.computed = true;
type.value = expressionTo.String(argumentPath);
} else {
type.value = argumentPath.get('elements').map(getPropType);
}
return type;
}
function getPropTypeArrayOf(path) {
types.CallExpression.assert(path.node);
var argumentPath = path.get('arguments', 0);
var type = {name: 'arrayof'};
var subType = getPropType(argumentPath);
if (subType.name === 'unknown') {
type.value = expressionTo.String(argumentPath);
type.computed = true;
} else {
type.value = subType;
}
return type;
}
function getPropTypeShape(path) {
types.CallExpression.assert(path.node);
var valuePath = path.get('arguments', 0);
var type: {name: string; value: any;} = {name: 'shape', value: 'unkown'};
if (!types.ObjectExpression.check(valuePath.node)) {
valuePath = resolveToValue(valuePath);
}
if (types.ObjectExpression.check(valuePath.node)) {
type.value = {};
valuePath.get('properties').each(function(propertyPath) {
type.value[getPropertyName(propertyPath)] =
getPropType(propertyPath.get('value'));
});
}
return type;
}
function getPropTypeInstanceOf(path) {
types.CallExpression.assert(path.node);
return {
name: 'instance',
value: expressionTo.String(path.get('arguments', 0))
};
}
var propTypes = {
oneOf: getPropTypeOneOf,
oneOfType: getPropTypeOneOfType,
instanceOf: getPropTypeInstanceOf,
arrayOf: getPropTypeArrayOf,
shape: getPropTypeShape
};
/**
* Tries to identify the prop type by the following rules:
*
* Member expressions which resolve to the `React` or `ReactPropTypes` module
* are inspected to see whether their properties are prop types. Strictly
* speaking we'd have to test whether the Member expression resolves to
* require('React').PropTypes, but we are not doing this right now for
* simplicity.
*
* Everything else is treated as custom validator
*/
function getPropType(path) {
var node = path.node;
if (types.FunctionExpression.check(node) || !isPropTypesExpression(path)) {
return {name: 'custom'};
}
var expressionParts = [];
if (types.MemberExpression.check(node)) {
// React.PropTypes.something.isRequired
if (isRequired(path)) {
path = path.get('object');
node = path.node;
}
// React.PropTypes.something
expressionParts = expressionTo.Array(path);
}
if (types.CallExpression.check(node)) {
// React.PropTypes.something()
expressionParts = expressionTo.Array(path.get('callee'));
}
// React.PropTypes.something -> something
var propType = expressionParts.pop();
var type;
if (propType in propTypes) {
type = propTypes[propType](path);
} else {
type = {name: (propType in simplePropTypes) ? propType : 'unknown'};
}
return type;
}
/**
* Returns true of the prop is required, according to its type defintion
*/
function isRequired(path) {
if (types.MemberExpression.check(path.node)) {
var expressionParts = expressionTo.Array(path);
if (expressionParts[expressionParts.length - 1] === 'isRequired') {
return true;
}
}
return false;
}
/**
* Handles member expressions of the form
*
* ComponentA.propTypes
*
* it resolves ComponentA to its module name and adds it to the "composes" entry
* in the documentation.
*/
function amendComposes(documentation, path) {
var node = path.node;
if (!types.MemberExpression.check(node) ||
getNameOrValue(path.get('property')) !== 'propTypes' ||
!types.Identifier.check(node.object)) {
return;
}
var moduleName = resolveToModule(path.get('object'));
if (moduleName) {
documentation.addComposes(moduleName);
}
}
function amendPropTypes(documentation, path) {
path.get('properties').each(function(propertyPath) {
switch (propertyPath.node.type) {
case types.Property.name:
var type = getPropType(propertyPath.get('value'));
if (type) {
var propDescriptor = documentation.getPropDescriptor(
getPropertyName(propertyPath)
);
propDescriptor.type = type;
propDescriptor.required = type.name !== 'custom' &&
isRequired(propertyPath.get('value'));
}
break;
case types.SpreadProperty.name:
var resolvedValuePath = resolveToValue(propertyPath.get('argument'));
switch (resolvedValuePath.node.type) {
case types.ObjectExpression.name: // normal object literal
amendPropTypes(documentation, resolvedValuePath);
break;
case types.MemberExpression.name:
amendComposes(documentation, resolvedValuePath);
break;
}
break;
}
});
}
function propTypeHandler(documentation: Documentation, path: NodePath) {
path = resolveToValue(path);
switch (path.node.type) {
case types.ObjectExpression.name:
amendPropTypes(documentation, path);
break;
case types.MemberExpression.name:
amendComposes(documentation, path);
}
}
module.exports = propTypeHandler;

39
website/react-docgen/lib/main.js vendored Normal file
View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* @flow
*/
"use strict";
/**
* Extractor for React documentation in JavaScript.
*/
var ReactDocumentationParser = require('./ReactDocumentationParser');
var parser = new ReactDocumentationParser();
parser.addHandler(
require('./handlers/propTypeHandler'),
'propTypes'
);
parser.addHandler(
require('./handlers/propDocBlockHandler'),
'propTypes'
);
parser.addHandler(
require('./handlers/defaultValueHandler'),
'getDefaultProps'
);
parser.addHandler(
require('./handlers/componentDocblockHandler')
);
module.exports = parser;

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
"use strict";
jest.autoMockOff();
describe('docblock', function() {
describe('getDoclets', function() {
var getDoclets;
beforeEach(function() {
getDoclets = require('../docblock').getDoclets;
});
it('extacts single line doclets', function() {
expect(getDoclets('@foo bar\n@bar baz'))
.toEqual({foo: 'bar', bar: 'baz'});
});
it('extacts multi line doclets', function() {
expect(getDoclets('@foo bar\nbaz\n@bar baz'))
.toEqual({foo: 'bar\nbaz', bar: 'baz'});
});
it('extacts boolean doclets', function() {
expect(getDoclets('@foo bar\nbaz\n@abc\n@bar baz'))
.toEqual({foo: 'bar\nbaz', abc: true, bar: 'baz'});
});
});
});

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* Helper functions to work with docblock comments.
* @flow
*/
"use strict";
var types = require('recast').types.namedTypes;
var docletPattern = /^@(\w+)(?:$|\s((?:[^](?!^@\w))*))/gmi;
function parseDocblock(str) {
var lines = str.split('\n');
for (var i = 0, l = lines.length; i < l; i++) {
lines[i] = lines[i].replace(/^\s*\*\s?/, '');
}
return lines.join('\n').trim();
}
/**
* Given a path, this function returns the closest preceding docblock if it
* exists.
*/
function getDocblock(path: NodePath): ?string {
if (path.node.comments) {
var comments = path.node.comments.leading.filter(function(comment) {
return comment.type === 'Block' && comment.value.indexOf('*\n') === 0;
});
if (comments.length > 0) {
return parseDocblock(comments[comments.length - 1].value);
}
}
return null;
}
/**
* Given a string, this functions returns an object with doclet names as keys
* and their "content" as values.
*/
function getDoclets(str: string): Object {
var doclets = Object.create(null);
var match = docletPattern.exec(str);
for (; match; match = docletPattern.exec(str)) {
doclets[match[1]] = match[2] || true;
}
return doclets;
}
exports.getDocblock = getDocblock;
exports.getDoclets = getDoclets;

View File

@ -0,0 +1,80 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* @flow
*/
"use strict";
var resolveToValue = require('./resolveToValue');
var types = require('recast').types.namedTypes;
/**
* Splits a MemberExpression or CallExpression into parts.
* E.g. foo.bar.baz becomes ['foo', 'bar', 'baz']
*/
function toArray(path: NodePath): Array<string> {
var parts = [path];
var result = [];
while (parts.length > 0) {
path = parts.shift();
var node = path.node;
if (types.CallExpression.check(node)) {
parts.push(path.get('callee'));
continue;
} else if (types.MemberExpression.check(node)) {
parts.push(path.get('object'));
if (node.computed) {
var resolvedPath = resolveToValue(path.get('property'));
if (resolvedPath !== undefined) {
result = result.concat(toArray(resolvedPath));
} else {
result.push('<computed>');
}
} else {
result.push(node.property.name);
}
continue;
} else if (types.Identifier.check(node)) {
result.push(node.name);
continue;
} else if (types.Literal.check(node)) {
result.push(node.raw);
continue;
} else if (types.ThisExpression.check(node)) {
result.push('this');
continue;
} else if (types.ObjectExpression.check(node)) {
var properties = path.get('properties').map(function(property) {
return toString(property.get('key')) +
': ' +
toString(property.get('value'));
});
result.push('{' + properties.join(', ') + '}');
continue;
} else if(types.ArrayExpression.check(node)) {
result.push('[' + path.get('elements').map(toString).join(', ') + ']');
continue;
}
}
return result.reverse();
}
/**
* Creates a string representation of a member expression.
*/
function toString(path: NodePath): string {
return toArray(path).join('.');
}
exports.String = toString;
exports.Array = toArray;

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* @flow
*/
"use strict";
var types = require('recast').types.namedTypes;
/**
* If node is an Identifier, it returns its name. If it is a literal, it returns
* its value.
*/
function getNameOrValue(path: NodePath, raw?: boolean): string {
var node = path.node;
switch (node.type) {
case types.Identifier.name:
return node.name;
case types.Literal.name:
return raw ? node.raw : node.value;
default:
throw new TypeError('Argument must be an Identifier or a Literal');
}
}
module.exports = getNameOrValue;

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* @flow
*/
"use strict";
var getNameOrValue = require('./getNameOrValue');
var types = require('recast').types.namedTypes;
/**
* In an ObjectExpression, the name of a property can either be an identifier
* or a literal (or dynamic, but we don't support those). This function simply
* returns the value of the literal or name of the identifier.
*/
function getPropertyName(propertyPath: NodePath): string {
if (propertyPath.node.computed) {
throw new TypeError('Propery name must be an Identifier or a Literal');
}
return getNameOrValue(propertyPath.get('key'), false);
}
module.exports = getPropertyName;

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* @flow
*/
"use strict";
var reactModules = ['react', 'react/addons'];
/**
* Takes a module name (string) and returns true if it refers to a root react
* module name.
*/
function isReactModuleName(moduleName: string): boolean {
return reactModules.some(function(reactModuleName) {
return reactModuleName === moduleName.toLowerCase();
});
}
module.exports = isReactModuleName;

40
website/react-docgen/lib/utils/match.js vendored Normal file
View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* @flow
*/
"use strict";
/**
* This function takes an AST node and matches it against "pattern". Pattern
* is simply a (nested) object literal and it is traversed to see whether node
* contains those (nested) properties with the provided values.
*/
function match(node: ASTNOde, pattern: Object): boolean {
if (!node) {
return false;
}
for (var prop in pattern) {
if (!node[prop]) {
return false;
}
if (pattern[prop] && typeof pattern[prop] === 'object') {
if (!match(node[prop], pattern[prop])) {
return false;
}
} else if (pattern[prop] !== pattern[prop]) {
return false;
}
}
return true;
}
module.exports = match;

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* @flow
*/
"use strict";
var match = require('./match');
var resolveToValue = require('./resolveToValue');
var types = require('recast').types.namedTypes;
/**
* Given a path (e.g. call expression, member expression or identifier),
* this function tries to find the name of module from which the "root value"
* was imported.
*/
function resolveToModule(path: NodePath): ?string {
var node = path.node;
switch (node.type) {
case types.VariableDeclarator.name:
if (node.init) {
return resolveToModule(path.get('init'));
}
break;
case types.CallExpression.name:
if (match(node.callee, {type: types.Identifier.name, name: 'require'})) {
return node['arguments'][0].value;
}
return resolveToModule(path.get('callee'));
case types.Identifier.name:
var valuePath = resolveToValue(path);
if (valuePath !== path) {
return resolveToModule(valuePath);
}
break;
case types.MemberExpression.name:
while (path && types.MemberExpression.check(path.node)) {
path = path.get('object');
}
if (path) {
return resolveToModule(path);
}
}
}
module.exports = resolveToModule;

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
* @flow
*/
"use strict";
var types = require('recast').types.namedTypes;
/**
* If the path is an identifier, it is resolved in the scope chain.
* If it is an assignment expression, it resolves to the right hand side.
*
* Else the path itself is returned.
*/
function resolveToValue(path: NodePath): NodePath {
var node = path.node;
if (types.AssignmentExpression.check(node)) {
if (node.operator === '=') {
return resolveToValue(node.get('right'));
}
} else if (types.Identifier.check(node)) {
var scope = path.scope.lookup(node.name);
if (scope) {
var bindings = scope.getBindings()[node.name];
if (bindings.length > 0) {
var parentPath = scope.getBindings()[node.name][0].parent;
if (types.VariableDeclarator.check(parentPath.node)) {
parentPath = parentPath.get('init');
}
return resolveToValue(parentPath);
}
}
}
return path;
}
module.exports = resolveToValue;

View File

@ -0,0 +1,35 @@
{
"name": "react-docgen",
"version": "1.0.0",
"description": "Extract information from React components for documentation generation",
"bin": {
"react-docgen": "bin/react-docgen.js"
},
"main": "dist/main.js",
"scripts": {
"watch": "jsx lib/ dist/ --harmony --strip-types -w",
"build": "rm -rf dist/ && jsx lib/ dist/ --harmony --strip-types --no-cache-dir",
"prepublish": "npm run build",
"test": "jest"
},
"keywords": [
"react",
"documentation"
],
"author": "Felix Kling",
"license": "BSD-3-Clause",
"dependencies": {
"async": "^0.9.0",
"node-dir": "^0.1.6",
"nomnom": "^1.8.1",
"recast": "^0.9.17"
},
"devDependencies": {
"jest-cli": "^0.2.2",
"react-tools": "^0.12.2"
},
"jest": {
"scriptPreprocessor": "./preprocessor",
"testPathDirs": ["lib"]
}
}

9
website/react-docgen/preprocessor.js vendored Normal file
View File

@ -0,0 +1,9 @@
"use strict";
var reactTools = require('react-tools');
function process(source) {
return reactTools.transform(source, {harmony: true, stripTypes: true});
}
exports.process = process;