Merge branch 'master' into copyright-patch

This commit is contained in:
Andrew Turner 2015-02-12 18:32:21 -06:00
commit fe0054f70f
40 changed files with 2750 additions and 199 deletions

View File

@ -4,6 +4,7 @@ title: Getting Started
layout: docs
category: Quick Start
permalink: docs/getting-started.html
next: navigatorios
---

2
website/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
src/react-native/docs/**
core/metadata.js

View File

@ -4,40 +4,11 @@
*/
var React = require('React');
var slugify = require('slugify');
var Header = React.createClass({
slug: function(string) {
// var accents = 'àáäâèéëêìíïîòóöôùúüûñç';
var accents = '\u00e0\u00e1\u00e4\u00e2\u00e8' +
'\u00e9\u00eb\u00ea\u00ec\u00ed\u00ef' +
'\u00ee\u00f2\u00f3\u00f6\u00f4\u00f9' +
'\u00fa\u00fc\u00fb\u00f1\u00e7';
var without = 'aaaaeeeeiiiioooouuuunc';
return string
.toString()
// Handle uppercase characters
.toLowerCase()
// Handle accentuated characters
.replace(
new RegExp('[' + accents + ']', 'g'),
function (c) { return without.charAt(accents.indexOf(c)); })
// Dash special characters
.replace(/[^a-z0-9]/g, '-')
// Compress multiple dash
.replace(/-+/g, '-')
// Trim dashes
.replace(/^-|-$/g, '');
},
render: function() {
var slug = this.slug(this.props.toSlug || this.props.children);
var slug = slugify(this.props.toSlug || this.props.children);
var H = React.DOM['h' + this.props.level];
return this.transferPropsTo(

View File

@ -53,7 +53,7 @@ var Site = React.createClass({
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-387204-10', 'facebook.github.io');
ga('create', 'UA-41298772-2', 'facebook.github.io');
ga('send', 'pageview');
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)

View File

@ -1,15 +0,0 @@
/**
* @generated
* @providesModule Metadata
*/
module.exports = {
"files": [
{
"id": "getting-started",
"title": "Getting Started",
"layout": "docs",
"category": "Quick Start",
"permalink": "docs/getting-started.html"
}
]
};

35
website/core/slugify.js Normal file
View File

@ -0,0 +1,35 @@
/**
* @providesModule slugify
*/
var slugify = function(string) {
// var accents = 'àáäâèéëêìíïîòóöôùúüûñç';
var accents = '\u00e0\u00e1\u00e4\u00e2\u00e8' +
'\u00e9\u00eb\u00ea\u00ec\u00ed\u00ef' +
'\u00ee\u00f2\u00f3\u00f6\u00f4\u00f9' +
'\u00fa\u00fc\u00fb\u00f1\u00e7';
var without = 'aaaaeeeeiiiioooouuuunc';
return string
.toString()
// Handle uppercase characters
.toLowerCase()
// Handle accentuated characters
.replace(
new RegExp('[' + accents + ']', 'g'),
function (c) { return without.charAt(accents.indexOf(c)); })
// Dash special characters
.replace(/[^a-z0-9]/g, '-')
// Compress multiple dash
.replace(/-+/g, '-')
// Trim dashes
.replace(/^-|-$/g, '');
};
module.exports = slugify;

1
website/react-docgen/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist/

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,178 @@
# react-docgen
`react-docgen` extracts information from React components with which
you can generate documentation for those components.
It uses [recast][] 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-docgen
```
## CLI
Installing the module adds a `react-docgen` 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-docgen [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-docgen');
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>]
["raw": string]
},
"required": boolean,
"description": string,
["defaultValue": {
"value": number | string,
"computed": boolean
}]
},
...
},
["composes": <componentNames>]
}
```
[recast]: https://github.com/benjamn/recast

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-docgen')
.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,73 @@
/*
* 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;
raw?: string;
};
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,474 @@
/*
* 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('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',
raw: 'xyz'
}]
},
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',
raw: 'xyz'
},
}
},
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() {},',
' custom_prop2: () => {}',
' }',
'}'
].join('\n'));
var expectedResult = {
description: '',
props: {
custom_prop: {
type: {
name: 'custom',
raw: 'function() {}'
},
required: false
},
custom_prop2: {
type: {
name: 'custom',
raw: '() => {}'
},
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',
raw: 'Prop.bool.isRequired'
},
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 = recast.print(path).code;
}
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,259 @@
/*
* 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 recast = require('recast');
var resolveToModule = require('../utils/resolveToModule');
var resolveToValue = require('../utils/resolveToValue');
var types = 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.Function.check(node) || !isPropTypesExpression(path)) {
return {
name: 'custom',
raw: recast.print(path).code
};
}
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;

View File

@ -3,6 +3,7 @@ var glob = require('glob');
var mkdirp = require('mkdirp');
var optimist = require('optimist');
var path = require('path');
var extractDocs = require('./extractDocs');
var argv = optimist.argv;
function splitHeader(content) {
@ -28,89 +29,93 @@ function backtickify(str) {
function execute() {
var MD_DIR = '../docs/';
glob('src/react-native/docs/*.*', function(er, files) {
files.forEach(function(file) {
try {
fs.unlinkSync(file);
} catch(e) {
/* seriously, unlink throws when the file doesn't exist :( */
}
});
var files = glob.sync('src/react-native/docs/*.*')
files.forEach(function(file) {
try {
fs.unlinkSync(file);
} catch(e) {
/* seriously, unlink throws when the file doesn't exist :( */
}
});
var metadatas = {
files: [],
};
glob(MD_DIR + '**/*.*', function (er, files) {
files.forEach(function(file) {
var extension = path.extname(file);
if (extension === '.md' || extension === '.markdown') {
var content = fs.readFileSync(file, {encoding: 'utf8'});
var metadata = {};
function handleMarkdown(content) {
var metadata = {};
// Extract markdown metadata header
var both = splitHeader(content);
var lines = both.header.split('\n');
for (var i = 0; i < lines.length - 1; ++i) {
var keyvalue = lines[i].split(':');
var key = keyvalue[0].trim();
var value = keyvalue.slice(1).join(':').trim();
// Handle the case where you have "Community #10"
try { value = JSON.parse(value); } catch(e) { }
metadata[key] = value;
}
metadatas.files.push(metadata);
// Extract markdown metadata header
var both = splitHeader(content);
var lines = both.header.split('\n');
for (var i = 0; i < lines.length - 1; ++i) {
var keyvalue = lines[i].split(':');
var key = keyvalue[0].trim();
var value = keyvalue.slice(1).join(':').trim();
// Handle the case where you have "Community #10"
try { value = JSON.parse(value); } catch(e) { }
metadata[key] = value;
}
metadatas.files.push(metadata);
if (metadata.permalink.match(/^https?:/)) {
return;
}
if (metadata.permalink.match(/^https?:/)) {
return;
}
// Create a dummy .js version that just calls the associated layout
var layout = metadata.layout[0].toUpperCase() + metadata.layout.substr(1) + 'Layout';
// Create a dummy .js version that just calls the associated layout
var layout = metadata.layout[0].toUpperCase() + metadata.layout.substr(1) + 'Layout';
var content = (
'/**\n' +
' * @generated\n' +
' * @jsx React.DOM\n' +
' */\n' +
'var React = require("React");\n' +
'var layout = require("' + layout + '");\n' +
'var content = ' + backtickify(both.content) + '\n' +
'var Post = React.createClass({\n' +
' render: function() {\n' +
' return layout({metadata: ' + JSON.stringify(metadata) + '}, content);\n' +
' }\n' +
'});\n' +
// TODO: Use React statics after upgrading React
'Post.content = content;\n' +
'module.exports = Post;\n'
);
var targetFile = 'src/react-native/' + metadata.permalink.replace(/\.html$/, '.js');
mkdirp.sync(targetFile.replace(new RegExp('/[^/]*$'), ''));
fs.writeFileSync(targetFile, content);
}
if (extension === '.json') {
var content = fs.readFileSync(file, {encoding: 'utf8'});
metadatas[path.basename(file, '.json')] = JSON.parse(content);
}
});
fs.writeFileSync(
'core/metadata.js',
var content = (
'/**\n' +
' * @generated\n' +
' * @providesModule Metadata\n' +
' * @jsx React.DOM\n' +
' */\n' +
'module.exports = ' + JSON.stringify(metadatas, null, 2) + ';'
'var React = require("React");\n' +
'var layout = require("' + layout + '");\n' +
'var content = ' + backtickify(both.content) + '\n' +
'var Post = React.createClass({\n' +
' render: function() {\n' +
' return layout({metadata: ' + JSON.stringify(metadata) + '}, content);\n' +
' }\n' +
'});\n' +
// TODO: Use React statics after upgrading React
'Post.content = content;\n' +
'module.exports = Post;\n'
);
var targetFile = 'src/react-native/' + metadata.permalink.replace(/\.html$/, '.js');
mkdirp.sync(targetFile.replace(new RegExp('/[^/]*$'), ''));
fs.writeFileSync(targetFile, content);
}
extractDocs().forEach(handleMarkdown);
var files = glob.sync(MD_DIR + '**/*.*');
files.forEach(function(file) {
var extension = path.extname(file);
if (extension === '.md' || extension === '.markdown') {
var content = fs.readFileSync(file, {encoding: 'utf8'});
handleMarkdown(content);
}
if (extension === '.json') {
var content = fs.readFileSync(file, {encoding: 'utf8'});
metadatas[path.basename(file, '.json')] = JSON.parse(content);
}
});
fs.writeFileSync(
'core/metadata.js',
'/**\n' +
' * @generated\n' +
' * @providesModule Metadata\n' +
' */\n' +
'module.exports = ' + JSON.stringify(metadatas, null, 2) + ';'
);
}
if (argv.convert) {
console.log('convert!')
console.log('convert!');
execute();
}

View File

@ -0,0 +1,53 @@
var docs = require('../react-docgen');
var fs = require('fs');
var path = require('path');
var slugify = require('../core/slugify');
function getNameFromPath(filepath) {
var ext = null;
while (ext = path.extname(filepath)) {
filepath = path.basename(filepath, ext);
}
return filepath;
}
function docsToMarkdown(filepath, i) {
var json = docs.parseSource(fs.readFileSync(filepath));
var componentName = getNameFromPath(filepath);
var res = [
'---',
'id: ' + slugify(componentName),
'title: ' + componentName,
'layout: docs',
'category: Components',
'permalink: docs/' + slugify(componentName) + '.html',
components[i + 1] && ('next: ' + slugify(getNameFromPath(components[i + 1]))),
'---',
' ',
json.description,
' ',
'# Props',
'```',
JSON.stringify(json.props, null, 2),
'```',
].filter(function(line) { return line; }).join('\n');
return res;
}
var components = [
'../Libraries/Components/Navigation/NavigatorIOS.ios.js',
'../Libraries/Components/Image/Image.ios.js',
'../Libraries/Components/ListView/ListView.js',
'../Libraries/Components/Navigation/NavigatorIOS.ios.js',
'../Libraries/Components/ScrollView/ScrollView.ios.js',
'../Libraries/Components/Text/Text.js',
'../Libraries/Components/TextInput/TextInput.ios.js',
'../Libraries/Components/Touchable/TouchableHighlight.js',
'../Libraries/Components/Touchable/TouchableWithoutFeedback.js',
// '../Libraries/Components/View/View.js',
];
module.exports = function() {
return components.map(docsToMarkdown);
};

View File

@ -26,7 +26,7 @@ var queue = (function() {
is_executing = true;
fn(function() {
is_executing = false;
execute()
execute();
});
}
return {push: push};

View File

@ -1,85 +0,0 @@
/**
* @generated
* @jsx React.DOM
*/
var React = require("React");
var layout = require("DocsLayout");
var content = `
Our first React Native implementation is \`ReactKit\`, targeting iOS. We are also
working on an Android implementation which we will release later. \`ReactKit\`
apps are built using the [React JS](https://github.com/facebook/react) framework, and render directly to
native UIKit elements using a fully asynchronous architecture. There is no
browser and no HTML. We have picked what we think is the best set of features
from these and other technologies to build what we hope to become the best
product development framework available, with an emphasis on iteration speed,
developer delight, continuity of technology, and absolutely beautiful and fast
products with no compromises in quality or capability.
## Requirements
1. OS X - This repo only contains the iOS implementation right now, and Xcode only runs on Mac.
2. New to Xcode? [Download it](https://developer.apple.com/xcode/downloads/) from the Mac App Store.
3. [Homebrew](http://brew.sh/) is the recommended way to install node, watchman, and flow.
4. New to node or npm? \`brew install node\`
5. We recommend installing [watchman](https://facebook.github.io/watchman/docs/install.html), otherwise you might hit a node file watching bug. \`brew install watchman\`
6. If you want to use [flow](http://www.flowtype.org), \`brew install flow\`
## Quick start
Get up and running with our Movies sample app:
1. Once you have the repo cloned and met all the requirements above, start the
packager that will transform your JS code on-the-fly:
\`\`\`
npm install
npm start
\`\`\`
2. Open the \`Examples/Movies/Movies.xcodeproj\` project in Xcode.
3. Make sure the target is set to \`Movies\` and that you have an iOS simulator
selected to run the app.
4. Build and run the project with the Xcode run button.
You should now see the Movies app running on your iOS simulator.
Congratulations! You've just successfully run your first React Native app.
Now try editing a JavaScript file and viewing your changes. Let's change the
movie search placeholder text:
1. Open the \`Examples/Movies/SearchScreen.js\` file in your favorite JavaScript
editor.
2. Look for the current search placeholder text and change it to "Search for an
awesome movie...".
3. Hit cmd+R ([twice](http://openradar.appspot.com/19613391)) in your iOS simulator to reload the app and see your change.
If you don't immediately see your changes, try restarting your app within Xcode.
Feel free to browse the Movies sample files and customize various properties to
get familiar with the codebase and React Native.
Also check out the UI Component Explorer for more sample code:
\`Examples/UIExplorer/UIExplorer.xcodeproj\`. **Make sure to close the Movies
project first - Xcode will break if you have two projects open that reference
the same library.**
## Troubleshooting
+ Xcode will break if you have two examples open at the same time.
+ If \`npm start\` fails with log spew like:
\`\`\`
2015-02-02 10:56 node[24294] (FSEvents.framework) FSEventStreamStart: register_with_server: ERROR: f2d_register_rpc() => (null) (-21)
\`\`\`
then you've hit the node file watching bug - \`brew install watchman\` should fix the issue.
+ Jest testing does not yet work on node versions after 0.10.x.
+ You can verify the packager is working by loading the [bundle](http://localhost:8081/Examples/Movies/MoviesApp.includeRequire.runModule.bundle) in your browser and
inspecting the contents.
Please report any other issues you encounter so we can fix them ASAP.
`
var Post = React.createClass({
render: function() {
return layout({metadata: {"id":"getting-started","title":"Getting Started","layout":"docs","category":"Quick Start","permalink":"docs/getting-started.html"}}, content);
}
});
Post.content = content;
module.exports = Post;

View File

@ -32,7 +32,7 @@ var support = React.createClass({
<H2>Twitter</H2>
<p><a href="https://twitter.com/search?q=%23reactnative"><strong>#reactnative</strong> hash tag on Twitter</a> is used to keep up with the latest React Native news.</p>
<p><center><a className="twitter-timeline" data-dnt="true" data-chrome="nofooter noheader transparent" href="https://twitter.com/search?q=%23react+native" data-widget-id="464145350849085440"></a></center></p>
<p><center><a className="twitter-timeline" data-dnt="true" data-chrome="nofooter noheader transparent" href="https://twitter.com/search?q=%23reactnative" data-widget-id="565960513457098753"></a></center></p>
</div>
</section>