diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md
index 24365afcc..eda46461b 100644
--- a/docs/GettingStarted.md
+++ b/docs/GettingStarted.md
@@ -4,6 +4,7 @@ title: Getting Started
layout: docs
category: Quick Start
permalink: docs/getting-started.html
+next: navigatorios
---
diff --git a/website/.gitignore b/website/.gitignore
new file mode 100644
index 000000000..37a25d50a
--- /dev/null
+++ b/website/.gitignore
@@ -0,0 +1,2 @@
+src/react-native/docs/**
+core/metadata.js
diff --git a/website/core/Header.js b/website/core/Header.js
index 0a2bf3a17..f49cae374 100644
--- a/website/core/Header.js
+++ b/website/core/Header.js
@@ -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(
diff --git a/website/core/Site.js b/website/core/Site.js
index eac71e9f4..98342ba4c 100644
--- a/website/core/Site.js
+++ b/website/core/Site.js
@@ -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)
diff --git a/website/core/metadata.js b/website/core/metadata.js
deleted file mode 100644
index e3c129eaf..000000000
--- a/website/core/metadata.js
+++ /dev/null
@@ -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"
- }
- ]
-};
\ No newline at end of file
diff --git a/website/core/slugify.js b/website/core/slugify.js
new file mode 100644
index 000000000..9f9c48b6f
--- /dev/null
+++ b/website/core/slugify.js
@@ -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;
diff --git a/website/react-docgen/.gitignore b/website/react-docgen/.gitignore
new file mode 100644
index 000000000..849ddff3b
--- /dev/null
+++ b/website/react-docgen/.gitignore
@@ -0,0 +1 @@
+dist/
diff --git a/website/react-docgen/LICENSE b/website/react-docgen/LICENSE
new file mode 100644
index 000000000..17e428880
--- /dev/null
+++ b/website/react-docgen/LICENSE
@@ -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.
diff --git a/website/react-docgen/PATENTS b/website/react-docgen/PATENTS
new file mode 100644
index 000000000..f8ef30d1d
--- /dev/null
+++ b/website/react-docgen/PATENTS
@@ -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.
diff --git a/website/react-docgen/README.md b/website/react-docgen/README.md
new file mode 100644
index 000000000..e670a286e
--- /dev/null
+++ b/website/react-docgen/README.md
@@ -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": {
+ "": {
+ "type": {
+ "name": "",
+ ["value": ]
+ ["raw": string]
+ },
+ "required": boolean,
+ "description": string,
+ ["defaultValue": {
+ "value": number | string,
+ "computed": boolean
+ }]
+ },
+ ...
+ },
+ ["composes": ]
+}
+```
+
+[recast]: https://github.com/benjamn/recast
diff --git a/website/react-docgen/bin/react-docgen.js b/website/react-docgen/bin/react-docgen.js
new file mode 100755
index 000000000..4921f67ad
--- /dev/null
+++ b/website/react-docgen/bin/react-docgen.js
@@ -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);
+ }
+});
diff --git a/website/react-docgen/flow/recast.js b/website/react-docgen/flow/recast.js
new file mode 100644
index 000000000..8d87bb65c
--- /dev/null
+++ b/website/react-docgen/flow/recast.js
@@ -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>;
+}
+
+declare class NodePath {
+ node: ASTNode;
+ parent: NodePath;
+ scope: Scope;
+
+ get(...x: (string|number)): NodePath;
+ each(f: (p: NodePath) => void): void;
+ map(f: (p: NodePath) => T): Array;
+}
diff --git a/website/react-docgen/lib/Documentation.js b/website/react-docgen/lib/Documentation.js
new file mode 100644
index 000000000..d23d28e40
--- /dev/null
+++ b/website/react-docgen/lib/Documentation.js
@@ -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;
+
+ 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;
diff --git a/website/react-docgen/lib/ReactDocumentationParser.js b/website/react-docgen/lib/ReactDocumentationParser.js
new file mode 100644
index 000000000..8e73d47d7
--- /dev/null
+++ b/website/react-docgen/lib/ReactDocumentationParser.js
@@ -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;
+ _propertyHandlers: Object;
+
+ 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;
diff --git a/website/react-docgen/lib/__tests__/ReactDocumentationParser-test.js b/website/react-docgen/lib/__tests__/ReactDocumentationParser-test.js
new file mode 100644
index 000000000..52ede4145
--- /dev/null
+++ b/website/react-docgen/lib/__tests__/ReactDocumentationParser-test.js
@@ -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();
+ });
+});
diff --git a/website/react-docgen/lib/handlers/__tests__/componentDocblockHandler-test.js b/website/react-docgen/lib/handlers/__tests__/componentDocblockHandler-test.js
new file mode 100644
index 000000000..fd0dfd1d6
--- /dev/null
+++ b/website/react-docgen/lib/handlers/__tests__/componentDocblockHandler-test.js
@@ -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);
+ });
+});
diff --git a/website/react-docgen/lib/handlers/__tests__/defaultValueHandler-test.js b/website/react-docgen/lib/handlers/__tests__/defaultValueHandler-test.js
new file mode 100644
index 000000000..7902d3ee7
--- /dev/null
+++ b/website/react-docgen/lib/handlers/__tests__/defaultValueHandler-test.js
@@ -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);
+ });
+});
diff --git a/website/react-docgen/lib/handlers/__tests__/propDocblockHandler-test.js b/website/react-docgen/lib/handlers/__tests__/propDocblockHandler-test.js
new file mode 100644
index 000000000..90f035b53
--- /dev/null
+++ b/website/react-docgen/lib/handlers/__tests__/propDocblockHandler-test.js
@@ -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);
+ });
+});
diff --git a/website/react-docgen/lib/handlers/__tests__/propTypeHandler-test.js b/website/react-docgen/lib/handlers/__tests__/propTypeHandler-test.js
new file mode 100644
index 000000000..7555ab0e6
--- /dev/null
+++ b/website/react-docgen/lib/handlers/__tests__/propTypeHandler-test.js
@@ -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);
+ });
+});
diff --git a/website/react-docgen/lib/handlers/componentDocblockHandler.js b/website/react-docgen/lib/handlers/componentDocblockHandler.js
new file mode 100644
index 000000000..8cb876a06
--- /dev/null
+++ b/website/react-docgen/lib/handlers/componentDocblockHandler.js
@@ -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;
diff --git a/website/react-docgen/lib/handlers/defaultValueHandler.js b/website/react-docgen/lib/handlers/defaultValueHandler.js
new file mode 100644
index 000000000..dd51d58d7
--- /dev/null
+++ b/website/react-docgen/lib/handlers/defaultValueHandler.js
@@ -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;
diff --git a/website/react-docgen/lib/handlers/propDocBlockHandler.js b/website/react-docgen/lib/handlers/propDocBlockHandler.js
new file mode 100644
index 000000000..d1642e975
--- /dev/null
+++ b/website/react-docgen/lib/handlers/propDocBlockHandler.js
@@ -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;
diff --git a/website/react-docgen/lib/handlers/propTypeHandler.js b/website/react-docgen/lib/handlers/propTypeHandler.js
new file mode 100644
index 000000000..bd28a415a
--- /dev/null
+++ b/website/react-docgen/lib/handlers/propTypeHandler.js
@@ -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;
diff --git a/website/react-docgen/lib/main.js b/website/react-docgen/lib/main.js
new file mode 100644
index 000000000..d2b184c8b
--- /dev/null
+++ b/website/react-docgen/lib/main.js
@@ -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;
diff --git a/website/react-docgen/lib/utils/__tests__/docblock-test.js b/website/react-docgen/lib/utils/__tests__/docblock-test.js
new file mode 100644
index 000000000..23e3d1eb1
--- /dev/null
+++ b/website/react-docgen/lib/utils/__tests__/docblock-test.js
@@ -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'});
+ });
+ });
+
+});
diff --git a/website/react-docgen/lib/utils/docblock.js b/website/react-docgen/lib/utils/docblock.js
new file mode 100644
index 000000000..09f888a88
--- /dev/null
+++ b/website/react-docgen/lib/utils/docblock.js
@@ -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;
diff --git a/website/react-docgen/lib/utils/expressionTo.js b/website/react-docgen/lib/utils/expressionTo.js
new file mode 100644
index 000000000..aa713a81b
--- /dev/null
+++ b/website/react-docgen/lib/utils/expressionTo.js
@@ -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 {
+ 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('');
+ }
+ } 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;
diff --git a/website/react-docgen/lib/utils/getNameOrValue.js b/website/react-docgen/lib/utils/getNameOrValue.js
new file mode 100644
index 000000000..9c63d2fe6
--- /dev/null
+++ b/website/react-docgen/lib/utils/getNameOrValue.js
@@ -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;
diff --git a/website/react-docgen/lib/utils/getPropertyName.js b/website/react-docgen/lib/utils/getPropertyName.js
new file mode 100644
index 000000000..ae70396f4
--- /dev/null
+++ b/website/react-docgen/lib/utils/getPropertyName.js
@@ -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;
diff --git a/website/react-docgen/lib/utils/isReactModuleName.js b/website/react-docgen/lib/utils/isReactModuleName.js
new file mode 100644
index 000000000..1b9f8878b
--- /dev/null
+++ b/website/react-docgen/lib/utils/isReactModuleName.js
@@ -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;
diff --git a/website/react-docgen/lib/utils/match.js b/website/react-docgen/lib/utils/match.js
new file mode 100644
index 000000000..9365d0ff8
--- /dev/null
+++ b/website/react-docgen/lib/utils/match.js
@@ -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;
diff --git a/website/react-docgen/lib/utils/resolveToModule.js b/website/react-docgen/lib/utils/resolveToModule.js
new file mode 100644
index 000000000..d60769b33
--- /dev/null
+++ b/website/react-docgen/lib/utils/resolveToModule.js
@@ -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;
diff --git a/website/react-docgen/lib/utils/resolveToValue.js b/website/react-docgen/lib/utils/resolveToValue.js
new file mode 100644
index 000000000..4e352d75f
--- /dev/null
+++ b/website/react-docgen/lib/utils/resolveToValue.js
@@ -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;
diff --git a/website/react-docgen/package.json b/website/react-docgen/package.json
new file mode 100644
index 000000000..9974df250
--- /dev/null
+++ b/website/react-docgen/package.json
@@ -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"]
+ }
+}
diff --git a/website/react-docgen/preprocessor.js b/website/react-docgen/preprocessor.js
new file mode 100644
index 000000000..f827426d4
--- /dev/null
+++ b/website/react-docgen/preprocessor.js
@@ -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;
diff --git a/website/server/convert.js b/website/server/convert.js
index c3fceaa79..de04eb948 100644
--- a/website/server/convert.js
+++ b/website/server/convert.js
@@ -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();
}
diff --git a/website/server/extractDocs.js b/website/server/extractDocs.js
new file mode 100644
index 000000000..9b7c0609a
--- /dev/null
+++ b/website/server/extractDocs.js
@@ -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);
+};
diff --git a/website/server/generate.js b/website/server/generate.js
index ef60e1715..0ec921036 100644
--- a/website/server/generate.js
+++ b/website/server/generate.js
@@ -26,7 +26,7 @@ var queue = (function() {
is_executing = true;
fn(function() {
is_executing = false;
- execute()
+ execute();
});
}
return {push: push};
diff --git a/website/src/react-native/docs/getting-started.js b/website/src/react-native/docs/getting-started.js
deleted file mode 100644
index 19b6fc34b..000000000
--- a/website/src/react-native/docs/getting-started.js
+++ /dev/null
@@ -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;
diff --git a/website/src/react-native/support.js b/website/src/react-native/support.js
index 55d0c11cc..898715ab1 100644
--- a/website/src/react-native/support.js
+++ b/website/src/react-native/support.js
@@ -32,7 +32,7 @@ var support = React.createClass({
Twitter
#reactnative hash tag on Twitter is used to keep up with the latest React Native news.
-
+