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:
parent
bd53b4191f
commit
ba7021f6f8
|
@ -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.
|
|
@ -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 Facebook’s 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.
|
|
@ -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>]
|
||||
}
|
||||
```
|
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -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>;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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();
|
||||
});
|
||||
});
|
96
website/react-docgen/lib/handlers/__tests__/componentDocblockHandler-test.js
vendored
Normal file
96
website/react-docgen/lib/handlers/__tests__/componentDocblockHandler-test.js
vendored
Normal 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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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'});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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"]
|
||||
}
|
||||
}
|
|
@ -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;
|
Loading…
Reference in New Issue