├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── annotate.js
├── esprima-extractor.js
├── esprima-transformer.js
├── index.js
├── package.json
├── styles.css
├── tests
├── annotate.js
├── ast
│ ├── classic.js
│ ├── integration.js
│ └── modern.js
├── defaults.js
├── extract.js
├── fixtures
│ ├── classic.js
│ └── modern.js
├── index.js
└── js
│ ├── classic.js
│ ├── fixtures
│ ├── classic.js
│ └── modern.js
│ ├── integration.js
│ └── modern.js
└── transforms
├── annotationsFor.js
├── annotators
├── CallExpression.js
├── MemberExpression.js
└── index.js
├── defaulters
├── Literal.js
├── _util.js
└── index.js
├── prepend.js
├── propTypeKey.js
└── types
├── _util.js
├── arrayOf.js
├── index.js
├── instanceOf.js
├── objectOf.js
├── oneOf.js
├── oneOfType.js
└── shape.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.12"
4 | - "0.10"
5 | - "iojs"
6 |
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Skookum Digital Works, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-autodoc
2 |
3 | [](https://magnum.travis-ci.com/Skookum/react-autodocs)
4 |
5 | React autodoc is the foundation for generating the documentation for your React
6 | Components. React provides a built-in mechanism for runtime property validation.
7 | If you opt-in to using these, you can use this tool to automate your
8 | documentation similar to what JSDoc provides for raw functions.
9 |
10 | `react-autodoc` contains two pieces. The primary piece is the `Autodoc` React
11 | component.
12 |
13 | The second piece is opt-in. It is a webpack `esprima-loader` transformer that
14 | will modify your source code to include the annotations that Autodoc requires.
15 |
16 | ## Example
17 |
18 | ``` javascript
19 |
20 | // button.js
21 | var Button = React.createClass({
22 | propTypes: {
23 | state: React.PropTypes.oneOf(['active', 'disabled', 'focused']),
24 | modifier: React.PropTypes.oneOf(['primary', 'secondary']).isRequired,
25 | children: React.PropTypes.any.isRequired,
26 | },
27 |
28 | render() {
29 | // using Suit.css semantics
30 | var uiState = this.props.state ? `is-${this.props.state}` : '';
31 | var modifier = this.props.modifier ? `Button--${this.props.modifier}` : '';
32 | return (
33 |
36 | );
37 | }
38 | });
39 |
40 | // button.autodoc.js
41 |
42 | var React = require('react');
43 | var Autodoc = require('react-autodoc');
44 | var Button = require('./button');
45 |
46 | var AutodocButton = React.createClass({
47 | render() {
48 | return (
49 |
50 | );
51 | }
52 | });
53 | ```
54 |
55 | This will produce a table that looks like a richer version of the following:
56 |
57 |
58 | ## Autodoc for Button
59 |
60 | | Property Key | Type | Required | Default Value |
61 | |--------------|-------------------------------|----------|---------------|
62 | | state | enum | false | 'active' |
63 | | modifier | enum | true | |
64 | | children | any | true | |
65 |
66 | ---
67 |
68 | ## Why the webpack loader?
69 |
70 | Webpack is a module loader that understands your entire dependency graph. It
71 | also has great support for loaders and transformations for pretty much anything.
72 |
73 | Using webpack provides the convenience of builds for different environments so
74 | you don’t have to add the any overhead to your project in production, but can
75 | easily include in development or qa environments.
76 |
77 | The inline version of the propType annotations looks something like this:
78 |
79 | ```javascript
80 | propTypes: {
81 | state: (
82 | (var tmp = React.PropTypes.oneOf(['active', 'disabled', 'focused'])),
83 | tmp.annotations = {type: 'enum'}, tmp
84 | ),
85 | modifier: (
86 | (var tmp = React.PropTypes.oneOf(['primary', 'secondary']).isRequired),
87 | tmp.annotations = {type: 'enum', isRequired: true}, tmp
88 | ),
89 | children: (
90 | (var tmp = React.PropTypes.any.isRequired),
91 | (tmp.annotations = {type: 'any', isRequired: true}), tmp
92 | )
93 | }
94 | ```
95 |
96 | Alternatives to inline annotations that can still be explored are:
97 |
98 | 1. Output the annotated date to a dynamic file for `react-autodoc` which
99 | can resolve the annotations by looking up a given `ReactComponent.displayName`.
100 | 2. Monkey patching React.PropTypes with runtime hints to what properties are
101 | available. Some immediate trade-offs is we can’t provide rich views into
102 | `CallExpression` propTypes such as `oneOf` or `shape`.
103 |
104 | ## React Autodoc Expected Annotations
105 |
106 | The `tests/annotationsFor.js` file contains the expected annotations for
107 | Autodoc. Implementing this interface will give you the freedom to build on top
108 | of either side of the Autodoc. You are free to reimplement `` or
109 | add your own build-step transformations to handle the annotating.
110 |
111 | ## MIT License
112 |
113 | Copyright 2015 Skookum Digital Works, Inc. All Right Reserved
114 |
115 |
--------------------------------------------------------------------------------
/annotate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015, Skookum Digital Works, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | *
8 | * @providesModule annotate
9 | */
10 |
11 | /**
12 | * Annotate a given object
13 | * @param {function} propDef React.PropTypes.*
14 | * @param {Object} annotations Autodoc annotations
15 | * @example
16 | *
17 | * annotate(
18 | * React.PropTypes.string.isRequired,
19 | * { type: 'string',
20 | * isRequired: true,
21 | * defaultValue: 'Hello',
22 | * name: 'Salutations'
23 | * }
24 | */
25 | module.exports = function annotate(propDef, annotations) {
26 | propDef.annotation = annotations;
27 | return propDef;
28 | };
29 |
--------------------------------------------------------------------------------
/esprima-extractor.js:
--------------------------------------------------------------------------------
1 | var annotationsFor = require('./transforms/annotationsFor');
2 | var _extract = annotationsFor.extract;
3 | var _defaults = annotationsFor.defaults;
4 |
5 | var estemplate = require('estemplate');
6 | var estraverse = require('estraverse-fb');
7 | var prependAnnotationRequire = require('./transforms/prepend');
8 | var deepExtend = require('deep-extend');
9 |
10 | function isClassic(node) {
11 | return (
12 | node.type === 'CallExpression' &&
13 | (
14 | node.callee.type === 'MemberExpression' &&
15 | node.callee.property.name === 'createClass'
16 | )
17 | );
18 | }
19 |
20 | function isModern(node) {
21 | return (
22 | node.type === 'ClassDeclaration' &&
23 | (
24 | node.superClass &&
25 | node.superClass.type === 'MemberExpression' &&
26 | node.superClass.object.name === 'React' &&
27 | node.superClass.property.name === 'Component'
28 | )
29 | );
30 | }
31 |
32 | function extract(o, node) {
33 | o[node.key.name] = _extract(node);
34 | return o;
35 | }
36 |
37 | function defaults(o, node) {
38 | o[node.key.name] = _defaults(node);
39 | return o;
40 | }
41 |
42 | function extractNameFromProperties(properties){
43 | return (find(properties, function(node) {
44 | return node.key.name === 'displayName';
45 | }) || {}).name;
46 | }
47 |
48 | function extractNameFromVariableDeclarator(declarator) {
49 | return declarator.id && declarator.id.name;
50 | }
51 |
52 | function find(o, fn) {
53 | var result;
54 | o.some(function(n, i) {
55 | if (fn.apply(null, arguments)) {
56 | result = n;
57 | return true;
58 | }
59 | });
60 | return result;
61 | }
62 |
63 | var _ReactClassDeclarations = [];
64 | var Annotations = {};
65 |
66 | module.exports = {
67 | onComplete: function() {},
68 | type: 'traverse',
69 | enter: function(node, parent) {
70 | // PropTypes
71 | if (isClassic(node)) {
72 | var props = node.arguments[0].properties;
73 | var name = (
74 | extractNameFromProperties(props) ||
75 | extractNameFromVariableDeclarator(parent)
76 | ) || 'INFER_FROM_FILE';
77 |
78 | var propTypes = find(props, function(p) {
79 | return p.type === 'Property' && p.key.name === 'propTypes'
80 | });
81 |
82 | var defaultValues = find(props, function(p) {
83 | return p.type === 'Property' && p.key.name === 'getDefaultProps';
84 | });
85 |
86 | if (defaultValues) {
87 | defaultValues = find(defaultValues.value.body.body, function(p) {
88 | return p.type === 'ReturnStatement' && p.argument.type === 'ObjectExpression';
89 | });
90 | }
91 | if (propTypes) {
92 | Annotations[name] = propTypes.value.properties.reduce(extract, {});
93 | }
94 |
95 | if (defaultValues) {
96 | Annotations[name] || (Annotations[name] = {});
97 | deepExtend(Annotations[name], defaultValues.argument.properties.reduce(defaults, {}));
98 | }
99 |
100 | return estraverse.VisitorOption.Skip;
101 | }
102 |
103 | else if (isModern(node)) {
104 | _ReactClassDeclarations.push(node.id.name);
105 | return estraverse.VisitorOption.Skip;
106 | }
107 |
108 | else if (
109 | node.type === 'MemberExpression' &&
110 | node.property.name === 'propTypes'
111 | ) {
112 | if (_ReactClassDeclarations.indexOf(node.object.name) === -1) {
113 | throw new Error(
114 | 'Attempted to assign propTypes to unknown React Component ' +
115 | node.object.name
116 | );
117 | }
118 |
119 | Annotations[node.object.name] = parent.right.properties.reduce(extract, {});
120 | }
121 | else if (
122 | node.type === 'MemberExpression' &&
123 | node.property.name === 'defaultProps'
124 | ) {
125 | var name = node.object.name;
126 | if (_ReactClassDeclarations.indexOf(name) === -1) {
127 | throw new Error(
128 | 'Attempted to assign defaultProps to unknown React Component ' +
129 | name
130 | );
131 | }
132 |
133 | Annotations[name] || (Annotations[name] = {});
134 | deepExtend(Annotations[name], parent.right.properties.reduce(defaults, {}));
135 | }
136 | },
137 |
138 | leave: function(node, parent) {
139 | // clean up when you leave a file
140 | if (node.type === 'Program') {
141 | module.exports.onComplete(Annotations);
142 |
143 | _ReactClassDeclarations = [];
144 | Annotations = {};
145 | }
146 | }
147 | };
148 |
149 |
150 |
--------------------------------------------------------------------------------
/esprima-transformer.js:
--------------------------------------------------------------------------------
1 |
2 | var annotationsFor = require('./transforms/annotationsFor').annotate;
3 | var estemplate = require('estemplate');
4 | var prependAnnotationRequire = require('./transforms/prepend');
5 |
6 | function isClassic(node) {
7 | return (
8 | node.type === 'Property' &&
9 | node.key.name === 'propTypes' &&
10 | typeof node.value.properties !== 'undefined'
11 | );
12 | }
13 |
14 | function isClassicPropTypes(node) {
15 | return (
16 | node.type === 'Property' &&
17 | node.key.name === 'getDefaultProps'
18 | );
19 | }
20 |
21 | function isModern(node, parent) {
22 | return (
23 | node.type === 'MemberExpression' &&
24 | node.property.name === 'propTypes' &&
25 | parent.right && typeof parent.right.properties !== 'undefined'
26 | );
27 | }
28 |
29 | function annotate(node) {
30 | node.value = estemplate(
31 | 'AnnotatePropTypes(' +
32 | '<%= propTypes %>, ' +
33 | '<%= annotation %>' +
34 | ')', {
35 | propTypes: node.value,
36 | annotation: annotationsFor(node)
37 | }
38 | );
39 |
40 | node.value = node.value.body[0].expression;
41 | return node;
42 | }
43 |
44 | module.exports = {
45 | type: 'replace',
46 | enter: function(node, parent) {
47 | if (node.type === 'Program') {
48 | prependAnnotationRequire(node.body);
49 | }
50 |
51 | if (isClassic(node)) {
52 | node.value.properties = node.value.properties.map(annotate);
53 | return node;
54 | }
55 | else if (isModern(node, parent)) {
56 | parent.right.properties = parent.right.properties.map(annotate);
57 | return node;
58 | }
59 | else if (isClassicPropTypes(node)) {
60 | console.log(node.value.properties);
61 | console.log('isClassicPropTypes', node);
62 | }
63 | }
64 | };
65 |
66 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
4 |
5 | /* @flow */
6 | require("./styles.css");
7 |
8 | var React = require("react");
9 |
10 | var Autodoc = React.createClass({
11 | displayName: "Autodoc",
12 |
13 | renderDocs: function renderDocs() {
14 | var docString = this.props.component.docString;
15 |
16 | if (typeof docString === "undefined") {
17 | return null;
18 | }return React.createElement(
19 | "p",
20 | { className: "Autodoc-description" },
21 | docString
22 | );
23 | },
24 |
25 | renderPropType: function renderPropType(propType) {
26 | if (typeof propType === "undefined") {
27 | return;
28 | }
29 |
30 | if (Array.isArray(propType)) {
31 | return propType.map(function (p) {
32 | return React.createElement(
33 | "span",
34 | { className: "Autodoc-type is-enum" },
35 | p
36 | );
37 | });
38 | }
39 |
40 | if (typeof propType === "object") {
41 | return React.createElement(
42 | "pre",
43 | { className: "Autodoc-type is-shape" },
44 | "interface " + String.fromCharCode(123) + "\n",
45 | Object.keys(propType).map(function (k) {
46 | return [React.createElement(
47 | "span",
48 | { className: "Autodoc-interface-key" },
49 | " ",
50 | k,
51 | ": "
52 | ), React.createElement(
53 | "span",
54 | { className: "Autodoc-interface-type Autodoc-type is-" + propType[k] },
55 | propType[k]
56 | ), "\n"];
57 | }),
58 | "" + String.fromCharCode(125)
59 | );
60 | }
61 |
62 | return React.createElement(
63 | "span",
64 | { className: "Autodoc-type is-" + propType },
65 | propType
66 | );
67 | },
68 |
69 | renderProps: function renderProps() {
70 | var _this = this;
71 |
72 | var propTypes = this.props.component.propTypes;
73 |
74 | if (typeof propTypes === "undefined") {
75 | return null;
76 | }return React.createElement(
77 | "table",
78 | _extends({ className: "Autodoc-table" }, this.props),
79 | React.createElement(
80 | "thead",
81 | null,
82 | React.createElement(
83 | "tr",
84 | null,
85 | React.createElement(
86 | "td",
87 | null,
88 | "Property"
89 | ),
90 | React.createElement(
91 | "td",
92 | null,
93 | "Type"
94 | )
95 | )
96 | ),
97 | React.createElement(
98 | "tbody",
99 | null,
100 | Object.keys(propTypes).filter(function (p) {
101 | return propTypes[p].annotation;
102 | }).map(function (p) {
103 | var _propTypes$p$annotation = propTypes[p].annotation;
104 | var propType = _propTypes$p$annotation.propType;
105 | var isRequired = _propTypes$p$annotation.isRequired;
106 | var defaultValue = _propTypes$p$annotation.defaultValue;
107 |
108 | isRequired = isRequired ? "Required" : "Optional";
109 | return React.createElement(
110 | "tr",
111 | { key: p },
112 | React.createElement(
113 | "th",
114 | { className: "Autodoc-property" },
115 | p,
116 | React.createElement(
117 | "span",
118 | { className: "Autodoc-required is-" + isRequired },
119 | isRequired
120 | )
121 | ),
122 | React.createElement(
123 | "td",
124 | null,
125 | _this.renderPropType(propType)
126 | ),
127 | React.createElement(
128 | "td",
129 | { className: "Autodoc-default" },
130 | defaultValue
131 | )
132 | );
133 | })
134 | )
135 | );
136 | },
137 |
138 | render: function render() {
139 | var displayName = this.props.component.displayName;
140 |
141 | return React.createElement(
142 | "div",
143 | { className: "Autodoc" },
144 | React.createElement(
145 | "h2",
146 | { className: "Autodoc-title" },
147 | "Autodoc for ",
148 | displayName
149 | ),
150 | this.renderDocs(),
151 | this.renderProps()
152 | );
153 | }
154 |
155 | });
156 |
157 | module.exports = Autodoc;
158 |
159 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-autodoc",
3 | "version": "0.9.2",
4 | "description": "A React component and webpack loader to autogenerate documentation for your React components",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "node ./tests/"
8 | },
9 | "author": "Dustan Kasten ",
10 | "license": "MIT",
11 | "dependencies": {
12 | "deep-extend": "^0.3.2",
13 | "deep-merge": "^1.0.0",
14 | "esprima-fb": "^13001.1.0-dev-harmony-fb",
15 | "estemplate": "^0.4.0",
16 | "estraverse-fb": "^1.3.0",
17 | "object.assign": "^1.1.1"
18 | },
19 | "devDependencies": {
20 | "escodegen": "^1.6.1"
21 | },
22 | "peerDependencies": {
23 | "react": "*"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | /** @define Autodoc; use strict*/
2 |
3 | .Autodoc {
4 | box-sizing: border-box;
5 | }
6 |
7 | .Autodoc {}
8 |
9 | .Autodoc-title {}
10 | .Autodoc-table {}
11 |
12 | .Autodoc-table th,
13 | .Autodoc-table td {
14 | padding: 0.1666em 0.333em;;
15 | vertical-align: top;
16 | }
17 |
18 | .Autodoc-table thead td {
19 | padding-bottom: 0.666em;
20 | background: rgba(0, 0, 0, 0.05);
21 | border-bottom: 2px solid rgba(0, 0, 0, 0.08);
22 | }
23 |
24 | .Autodoc-table tr:first-child th,
25 | .Autodoc-table tr:first-child td {
26 | padding-top: 0.666em;
27 | }
28 |
29 | .Autodoc-property,
30 | .Autodoc-type {
31 | padding: 0.3333em;
32 | vertical-align: top;
33 | }
34 |
35 | .Autodoc-property {
36 | text-align: left;
37 | font-weight: 100;
38 | color: #000;
39 | min-width: 100px;
40 | }
41 |
42 | .Autodoc-type {
43 | display: inline-block;
44 | font-size: 0.91666em;
45 | font-weight: 300;
46 | margin: 0.0666em;
47 | text-align: center;
48 | }
49 |
50 | .Autodoc-required {
51 | display: block;
52 | font-size: 0.666em;
53 | line-height: 1.5;
54 | }
55 |
56 | .Autodoc-type {
57 | background: #ddd;
58 | border-radius: 3px;
59 | color: #444;
60 | padding: 0.2222em;
61 | }
62 |
63 | .Autodoc-type.is-object {
64 | background: #aa5939;
65 | color: #fff;
66 | }
67 |
68 | .Autodoc-type.is-array,
69 | .Autodoc-type.is-enum {
70 | background: #0f5738;
71 | color: #eee;
72 | }
73 |
74 | .Autodoc-type.is-func {
75 | background: #aa7939;
76 | color: #eee;
77 | }
78 |
79 | .Autodoc-type.is-bool {
80 | background: #496d89;
81 | color: #fff;
82 | }
83 |
84 | .Autodoc-type.is-number {
85 | background: #ffdbaa;
86 | color: #553100;
87 | }
88 |
89 | .Autodoc-type.is-string {
90 | background: #277553;
91 | color: #fff;
92 | }
93 |
94 | .Autodoc-type.is-date {
95 | background: #0f5738;
96 | color: #fff;
97 | }
98 |
99 | .Autodoc-required {}
100 |
101 | .Autodoc-required.is-Required {
102 | color: #666;
103 | font-weight: bold;
104 | }
105 |
106 | .Autodoc-required.is-Optional {
107 | color: #666;
108 | }
109 |
110 | .Autodoc-type.is-shape {
111 | background: transparent;
112 | padding: 0.3333em;
113 | text-align: left;
114 | }
115 |
116 | .Autodoc-type.is-shape .Autodoc-type {
117 | min-width: 0;
118 | }
119 |
120 |
--------------------------------------------------------------------------------
/tests/annotate.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var esprima = require('esprima-fb');
3 | var annotationsFor = require('../transforms/annotationsFor').annotate;
4 | var escodegen = require('escodegen');
5 |
6 | // TODO: support custom function() {}
7 | // TODO: support custom functions defined on PropTypes
8 | // eg. https://github.com/rackt/react-router/blob/master/build/npm/lib/PropTypes.js
9 |
10 | function getProps(ast) {
11 | return ast.body[0].declarations[0].init.properties[0];
12 | }
13 |
14 | function test(input, expected) {
15 | var ast = esprima.parse(input);
16 | var properties = getProps(ast);
17 |
18 | var output = annotationsFor(properties);
19 | var outputSource = escodegen.generate(output).replace(/\s+/g, '');
20 |
21 | var expectedSource = (typeof expected === 'string' ?
22 | expected :
23 | escodegen.generate(expected)).replace(/\s+/g, '');
24 |
25 | assert.equal(
26 | outputSource,
27 | expectedSource,
28 | 'expected ' + outputSource + ' to match ' + expectedSource
29 | );
30 | }
31 |
32 | function ObjectExpression() {
33 | var props = Array.prototype.slice.call(arguments, 0);
34 | var properties = [];
35 | for (var i = 0; i < props.length; i += 2) {
36 | properties.push({
37 | type: 'Property',
38 | key: {type: 'Identifier', name: props[i]},
39 | value: {type: 'Literal', value: props[i + 1]}
40 | });
41 | }
42 |
43 | return {
44 | type: 'ObjectExpression',
45 | properties: properties
46 | };
47 | }
48 |
49 |
50 | var ObjectExpressionTypes = [
51 | 'array',
52 | 'bool',
53 | 'func',
54 | 'number',
55 | 'object',
56 | 'string',
57 | 'node',
58 | 'element',
59 | 'any',
60 | ];
61 |
62 | var CallExpressionTypes = [
63 | [ 'instanceOf(MyComponent)',
64 | '{propType: \'MyComponent\'}',
65 | '{propType: \'MyComponent\', isRequired: true}',
66 | ],
67 | [ 'arrayOf(React.PropTypes.string)',
68 | '{propType: \'string[]\'}',
69 | '{propType: \'string[]\', isRequired: true}',
70 | ],
71 | [ 'arrayOf(React.PropTypes.number)',
72 | '{propType: \'number[]\'}',
73 | '{propType: \'number[]\', isRequired: true}',
74 | ],
75 | [ 'oneOfType([React.PropTypes.string, React.PropTypes.number])',
76 | '{propType: [\'string\', \'number\']}',
77 | '{propType: [\'string\', \'number\'], isRequired: true}',
78 | ],
79 | [ 'oneOfType([React.PropTypes.string, React.PropTypes.instanceOf(MyComponent)])',
80 | '{propType: [\'string\', \'MyComponent\']}',
81 | '{propType: [\'string\', \'MyComponent\'], isRequired: true}',
82 | ],
83 | [ 'oneOf(["Hello", 5])',
84 | '{propType: [\'Hello\', 5]}',
85 | '{propType: [\'Hello\', 5], isRequired: true}',
86 | ],
87 | [ 'objectOf(React.PropTypes.number)',
88 | '{propType: \'number{}\'}',
89 | '{propType: \'number{}\', isRequired: true}',
90 | ],
91 | [ 'shape({name: React.PropTypes.string, id: React.PropTypes.number})',
92 | '{propType: {name: \'string\', id: \'number\'}}',
93 | '{propType: {name: \'string\', id: \'number\'}, isRequired: true}',
94 | ]
95 | ];
96 |
97 | ObjectExpressionTypes.forEach(function(type) {
98 | test(
99 | 'var propTypes = {prop: React.PropTypes.' + type + '};',
100 | ObjectExpression('propType', type)
101 | );
102 |
103 | test(
104 | 'var propTypes = {prop: React.PropTypes.' + type + '.isRequired};',
105 | ObjectExpression('propType', type, 'isRequired', true)
106 | );
107 | });
108 |
109 | CallExpressionTypes.forEach(function(expr) {
110 | test(
111 | 'var propTypes = {prop: React.PropTypes.' + expr[0] + '};',
112 | expr[1]
113 | );
114 |
115 | test(
116 | 'var propTypes = {prop: React.PropTypes.' + expr[0] + '.isRequired};',
117 | expr[2]
118 | );
119 | });
120 |
121 |
--------------------------------------------------------------------------------
/tests/ast/classic.js:
--------------------------------------------------------------------------------
1 | require('./integration')('classic');
2 |
--------------------------------------------------------------------------------
/tests/ast/integration.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var readFile = require('fs').readFileSync;
3 | var esprima = require('esprima-fb');
4 | var estraverse = require('estraverse-fb');
5 | var escodegen = require('escodegen');
6 | var assert = require('assert');
7 |
8 | var results;
9 | var spyOnReturn = function(obj, method) {
10 | var orig = obj[method];
11 | results = [];
12 | obj[method] = function() {
13 | var result = orig.apply(obj, arguments);
14 | results.push(
15 | escodegen.generate(result)
16 | );
17 | return result;
18 | }
19 | }
20 |
21 | module.exports = function(type) {
22 | spyOnReturn(require('../../transforms/annotationsFor'), 'annotate');
23 |
24 | var contents = readFile(path.join(__dirname, '..', 'fixtures', type + '.js'), 'utf8');
25 |
26 | var transformer = require('../../esprima-transformer');
27 |
28 | var ast = estraverse[transformer.type](
29 | esprima.parse(contents),
30 | transformer
31 | );
32 |
33 | // TODO: add support for default values
34 | var expected = [
35 | '{propType: \'array\', defaultValue: \'[]\'}',
36 | '{propType: \'bool\', defaultValue: \'false\'}',
37 | '{propType: \'func\', defaultValue: \'this.props.clickHandler\'}',
38 | '{propType: \'number\', defaultValue: -1}',
39 | '{propType: \'object\', optionalObject: \'{}\'}',
40 | '{propType: \'string\', optionalString: \'Hello, React\'}',
41 | '{propType: \'node\'}',
42 | '{propType: \'element\'}',
43 | '{propType: \'Message\'}',
44 | '{propType: [\'News\', \'Photos\'], defaultValue: \'News\'}',
45 | '{propType: [\'string\', \'number\', \'Message\']}',
46 | '{propType: \'number[]\'}',
47 | '{propType: \'number{}\'}',
48 | '{propType: {color: \'string\', fontSize: \'number\'}}',
49 | '{propType: \'func\', isRequired: true}',
50 | '{propType: \'any\', isRequired: true}',
51 | ];
52 |
53 | if (results.length === 0) {
54 | throw new Error(
55 | 'No results were parsed. Expected ' + expected.length + ' annotations.'
56 | );
57 | }
58 |
59 | results.forEach(function(r, i) {
60 | var a = r.replace(/\s+/g, '');
61 | var b = expected[i].replace(/\s+/g, '');
62 | assert.equal(a, b, 'Expected ' + a + ' to be ' + b);
63 | });
64 | };
65 |
66 |
--------------------------------------------------------------------------------
/tests/ast/modern.js:
--------------------------------------------------------------------------------
1 | require('./integration')('modern');
2 |
3 |
--------------------------------------------------------------------------------
/tests/defaults.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var esprima = require('esprima-fb');
3 | var escodegen = require('escodegen');
4 | var defaultsFor = require('../transforms/annotationsFor').defaults;
5 |
6 | var code = [
7 | 'var defaultProps = {',
8 | 'optionalArray: [],',
9 | 'optionalBool: false,',
10 | 'optionalFunc: this.props.clickHandler,',
11 | 'optionalNumber: -1,',
12 | 'optionalObject: {},',
13 | 'optionalString: \'Hello, React\',',
14 | 'optionalEnum: \'News\',',
15 | '};'
16 | ].join('\n');
17 |
18 | var expectedOutputs = [
19 | ObjectExpression('defaultValue', '[]'),
20 | ObjectExpression('defaultValue', 'false'),
21 | ObjectExpression('defaultValue', 'this.props.clickHandler'),
22 | ObjectExpression('defaultValue', -1),
23 | ObjectExpression('defaultValue', '{}'),
24 | ObjectExpression('defaultValue', 'Hello, React'),
25 | ObjectExpression('defaultValue', 'News'),
26 | ];
27 |
28 | var getProps = function(node) {
29 | return node.body[0].declarations[0].init.properties;
30 | }
31 |
32 | function test(node, index) {
33 | var expected = expectedOutputs[index];
34 | var output = defaultsFor(node);
35 |
36 | assert.deepEqual(
37 | output,
38 | expected,
39 | 'expected \n' + JSON.stringify(output, 2) + '\n to match \n' + JSON.stringify(expected, 2)
40 | );
41 | }
42 |
43 | function ObjectExpression() {
44 | var o = {};
45 | o[arguments[0]] = arguments[1];
46 | return o;
47 |
48 | var props = Array.prototype.slice.call(arguments, 0);
49 | var properties = [];
50 | for (var i = 0; i < props.length; i += 2) {
51 | properties.push({
52 | type: 'Property',
53 | key: {type: 'Identifier', name: props[i]},
54 | value: {type: 'Literal', value: props[i + 1]}
55 | });
56 | }
57 |
58 | return {
59 | type: 'ObjectExpression',
60 | properties: properties
61 | };
62 | }
63 |
64 | var ast = esprima.parse(code);
65 | var properties = getProps(ast);
66 | properties.forEach(test);
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/tests/extract.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var esprima = require('esprima-fb');
3 | var extractFor = require('../transforms/annotationsFor').extract;
4 |
5 | // TODO: support custom function() {}
6 | // TODO: support custom functions defined on PropTypes
7 | // eg. https://github.com/rackt/react-router/blob/master/build/npm/lib/PropTypes.js
8 |
9 | function getProps(ast) {
10 | return ast.body[0].declarations[0].init.properties[0];
11 | }
12 |
13 | function test(input, expected) {
14 | var ast = esprima.parse(input);
15 | var properties = getProps(ast);
16 |
17 | var output = extractFor(properties);
18 | //console.log(output)
19 | //console.log(expected);
20 |
21 | assert.deepEqual(
22 | output,
23 | expected,
24 | 'expected ' + JSON.stringify(output) + ' to match ' + JSON.stringify(expected)
25 | );
26 | }
27 |
28 | function ObjectExpression() {
29 | var props = Array.prototype.slice.call(arguments, 0);
30 | var properties = [];
31 | for (var i = 0; i < props.length; i += 2) {
32 | properties.push({
33 | type: 'Property',
34 | key: {type: 'Identifier', name: props[i]},
35 | value: {type: 'Literal', value: props[i + 1]}
36 | });
37 | }
38 |
39 | return {
40 | type: 'ObjectExpression',
41 | properties: properties
42 | };
43 | }
44 |
45 |
46 | var ObjectExpressionTypes = [
47 | 'array',
48 | 'bool',
49 | 'func',
50 | 'number',
51 | 'object',
52 | 'string',
53 | 'node',
54 | 'element',
55 | 'any',
56 | ];
57 |
58 | var CallExpressionTypes = [
59 | [ 'instanceOf(MyComponent)',
60 | {propType: 'MyComponent'},
61 | {propType: 'MyComponent', isRequired: true},
62 | ],
63 | [ 'arrayOf(React.PropTypes.string)',
64 | {propType: 'string[]'},
65 | {propType: 'string[]', isRequired: true},
66 | ],
67 | [ 'arrayOf(React.PropTypes.number)',
68 | {propType: 'number[]'},
69 | {propType: 'number[]', isRequired: true},
70 | ],
71 | [ 'oneOfType([React.PropTypes.string, React.PropTypes.number])',
72 | {propType: ['string', 'number']},
73 | {propType: ['string', 'number'], isRequired: true},
74 | ],
75 | [ 'oneOfType([React.PropTypes.string, React.PropTypes.instanceOf(MyComponent)])',
76 | {propType: ['string', 'MyComponent']},
77 | {propType: ['string', 'MyComponent'], isRequired: true},
78 | ],
79 | [ 'oneOf(["Hello", 5])',
80 | {propType: ['Hello', 5]},
81 | {propType: ['Hello', 5], isRequired: true},
82 | ],
83 | [ 'objectOf(React.PropTypes.number)',
84 | {propType: 'number{}'},
85 | {propType: 'number{}', isRequired: true},
86 | ],
87 | [ 'shape({name: React.PropTypes.string, id: React.PropTypes.number})',
88 | {propType: {name: 'string', id: 'number'}},
89 | {propType: {name: 'string', id: 'number'}, isRequired: true},
90 | ]
91 | ];
92 |
93 | ObjectExpressionTypes.forEach(function(type) {
94 | test(
95 | 'var propTypes = {prop: React.PropTypes.' + type + '};',
96 | {propType: type}
97 | );
98 |
99 | test(
100 | 'var propTypes = {prop: React.PropTypes.' + type + '.isRequired};',
101 | {propType: type, isRequired: true}
102 | );
103 | });
104 |
105 | CallExpressionTypes.forEach(function(expr) {
106 | test(
107 | 'var propTypes = {prop: React.PropTypes.' + expr[0] + '};',
108 | expr[1]
109 | );
110 |
111 | test(
112 | 'var propTypes = {prop: React.PropTypes.' + expr[0] + '.isRequired};',
113 | expr[2]
114 | );
115 | });
116 |
117 |
--------------------------------------------------------------------------------
/tests/fixtures/classic.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test example from
3 | * http://facebook.github.io/react/docs/reusable-components.html#prop-validation
4 | */
5 |
6 | var ClassicExample = React.createClass({
7 | propTypes: {
8 | optionalArray: React.PropTypes.array,
9 | optionalBool: React.PropTypes.bool,
10 | optionalFunc: React.PropTypes.func,
11 | optionalNumber: React.PropTypes.number,
12 | optionalObject: React.PropTypes.object,
13 | optionalString: React.PropTypes.string,
14 |
15 | // Anything that can be rendered: numbers, strings, elements or an array
16 | // containing these types.
17 | optionalNode: React.PropTypes.node,
18 |
19 | // A React element.
20 | optionalElement: React.PropTypes.element,
21 |
22 | // You can also declare that a prop is an instance of a class. This uses
23 | // JS's instanceof operator.
24 | optionalMessage: React.PropTypes.instanceOf(Message),
25 |
26 | // You can ensure that your prop is limited to specific values by treating
27 | // it as an enum.
28 | optionalEnum: React.PropTypes.oneOf(['News', 'Photos']),
29 |
30 | // An object that could be one of many types
31 | optionalUnion: React.PropTypes.oneOfType([
32 | React.PropTypes.string,
33 | React.PropTypes.number,
34 | React.PropTypes.instanceOf(Message)
35 | ]),
36 |
37 | // An array of a certain type
38 | optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number),
39 |
40 | // An object with property values of a certain type
41 | optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number),
42 |
43 | // An object taking on a particular shape
44 | optionalObjectWithShape: React.PropTypes.shape({
45 | color: React.PropTypes.string,
46 | fontSize: React.PropTypes.number
47 | }),
48 |
49 | // You can chain any of the above with `isRequired` to make sure a warning
50 | // is shown if the prop isn't provided.
51 | requiredFunc: React.PropTypes.func.isRequired,
52 |
53 | // A value of any data type
54 | requiredAny: React.PropTypes.any.isRequired,
55 |
56 | // You can also specify a custom validator. It should return an Error
57 | // object if the validation fails. Don't `console.warn` or throw, as this
58 | // won't work inside `oneOfType`.
59 | /*
60 | customProp: function(props, propName, componentName) {
61 | if (!/matchme/.test(props[propName])) {
62 | return new Error('Validation failed!');
63 | }
64 | }
65 | */
66 | },
67 |
68 | getDefaultProps: function() {
69 | return {
70 | optionalArray: [],
71 | optionalBool: false,
72 | optionalFunc: this.props.clickHandler,
73 | optionalNumber: -1,
74 | optionalObject: {},
75 | optionalString: 'Hello, React',
76 | optionalEnum: 'News',
77 | };
78 | },
79 |
80 | render() {
81 | return null;
82 | }
83 | });
84 |
85 |
--------------------------------------------------------------------------------
/tests/fixtures/modern.js:
--------------------------------------------------------------------------------
1 |
2 | class ModernExample extends React.Component {
3 | render() {
4 | return null;
5 | }
6 | }
7 |
8 | ModernExample.propTypes = {
9 | optionalArray: React.PropTypes.array,
10 | optionalBool: React.PropTypes.bool,
11 | optionalFunc: React.PropTypes.func,
12 | optionalNumber: React.PropTypes.number,
13 | optionalObject: React.PropTypes.object,
14 | optionalString: React.PropTypes.string,
15 |
16 | // Anything that can be rendered: numbers, strings, elements or an array
17 | // containing these types.
18 | optionalNode: React.PropTypes.node,
19 |
20 | // A React element.
21 | optionalElement: React.PropTypes.element,
22 |
23 | // You can also declare that a prop is an instance of a class. This uses
24 | // JS's instanceof operator.
25 | optionalMessage: React.PropTypes.instanceOf(Message),
26 |
27 | // You can ensure that your prop is limited to specific values by treating
28 | // it as an enum.
29 | optionalEnum: React.PropTypes.oneOf(['News', 'Photos']),
30 |
31 | // An object that could be one of many types
32 | optionalUnion: React.PropTypes.oneOfType([
33 | React.PropTypes.string,
34 | React.PropTypes.number,
35 | React.PropTypes.instanceOf(Message)
36 | ]),
37 |
38 | // An array of a certain type
39 | optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number),
40 |
41 | // An object with property values of a certain type
42 | optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number),
43 |
44 | // An object taking on a particular shape
45 | optionalObjectWithShape: React.PropTypes.shape({
46 | color: React.PropTypes.string,
47 | fontSize: React.PropTypes.number
48 | }),
49 |
50 | // You can chain any of the above with `isRequired` to make sure a warning
51 | // is shown if the prop isn't provided.
52 | requiredFunc: React.PropTypes.func.isRequired,
53 |
54 | // A value of any data type
55 | requiredAny: React.PropTypes.any.isRequired,
56 |
57 | // You can also specify a custom validator. It should return an Error
58 | // object if the validation fails. Don't `console.warn` or throw, as this
59 | // won't work inside `oneOfType`.
60 | /*
61 | customProp: function(props, propName, componentName) {
62 | if (!/matchme/.test(props[propName])) {
63 | return new Error('Validation failed!');
64 | }
65 | }
66 | */
67 | };
68 |
69 | ModernExample.defaultProps = {
70 | optionalArray: [],
71 | optionalBool: false,
72 | optionalFunc: this.props.clickHandler,
73 | optionalNumber: -1,
74 | optionalObject: {},
75 | optionalString: 'Hello, React',
76 | optionalEnum: 'News',
77 | };
78 |
79 |
--------------------------------------------------------------------------------
/tests/index.js:
--------------------------------------------------------------------------------
1 | // annotations
2 | require('./annotate');
3 | require('./extract');
4 |
5 | // defaults
6 | require('./defaults');
7 |
8 | // ast integration
9 | require('./ast/classic');
10 | require('./ast/modern');
11 |
12 |
13 | // js integration
14 | require('./js/classic');
15 | require('./js/modern');
16 |
17 |
--------------------------------------------------------------------------------
/tests/js/classic.js:
--------------------------------------------------------------------------------
1 | require('./integration')('classic');
2 |
--------------------------------------------------------------------------------
/tests/js/fixtures/classic.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test example from
3 | * http://facebook.github.io/react/docs/reusable-components.html#prop-validation
4 | */
5 |
6 | var ClassicExample = React.createClass({
7 | propTypes: {
8 | optionalArray: React.PropTypes.array,
9 | optionalBool: React.PropTypes.bool,
10 | optionalFunc: React.PropTypes.func,
11 | optionalNumber: React.PropTypes.number,
12 | optionalObject: React.PropTypes.object,
13 | optionalString: React.PropTypes.string,
14 |
15 | // Anything that can be rendered: numbers, strings, elements or an array
16 | // containing these types.
17 | optionalNode: React.PropTypes.node,
18 |
19 | // A React element.
20 | optionalElement: React.PropTypes.element,
21 |
22 | // You can also declare that a prop is an instance of a class. This uses
23 | // JS's instanceof operator.
24 | optionalMessage: React.PropTypes.instanceOf(Message),
25 |
26 | // You can ensure that your prop is limited to specific values by treating
27 | // it as an enum.
28 | optionalEnum: React.PropTypes.oneOf(['News', 'Photos']),
29 |
30 | // An object that could be one of many types
31 | optionalUnion: React.PropTypes.oneOfType([
32 | React.PropTypes.string,
33 | React.PropTypes.number,
34 | React.PropTypes.instanceOf(Message)
35 | ]),
36 |
37 | // An array of a certain type
38 | optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number),
39 |
40 | // An object with property values of a certain type
41 | optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number),
42 |
43 | // An object taking on a particular shape
44 | optionalObjectWithShape: React.PropTypes.shape({
45 | color: React.PropTypes.string,
46 | fontSize: React.PropTypes.number
47 | }),
48 |
49 | // You can chain any of the above with `isRequired` to make sure a warning
50 | // is shown if the prop isn't provided.
51 | requiredFunc: React.PropTypes.func.isRequired,
52 |
53 | // A value of any data type
54 | requiredAny: React.PropTypes.any.isRequired,
55 |
56 | // You can also specify a custom validator. It should return an Error
57 | // object if the validation fails. Don't `console.warn` or throw, as this
58 | // won't work inside `oneOfType`.
59 | /*
60 | customProp: function(props, propName, componentName) {
61 | if (!/matchme/.test(props[propName])) {
62 | return new Error('Validation failed!');
63 | }
64 | }
65 | */
66 | },
67 |
68 | getDefaultProps: function() {
69 | return {
70 | optionalArray: [],
71 | optionalBool: false,
72 | optionalFunc: this.props.clickHandler,
73 | optionalNumber: -1,
74 | optionalObject: {},
75 | optionalString: 'Hello, React',
76 | optionalEnum: 'News',
77 | };
78 | },
79 |
80 | render() {
81 | return null;
82 | }
83 | });
84 |
85 | /**
86 | * Test example from
87 | * http://facebook.github.io/react/docs/reusable-components.html#prop-validation
88 | */
89 |
90 | var Round2 = React.createClass({
91 | propTypes: {
92 | optionalArray: React.PropTypes.array,
93 | },
94 |
95 | getDefaultProps: function() {
96 | return {
97 | optionalArray: [],
98 | optionalBool: false,
99 | optionalFunc: this.props.clickHandler,
100 | optionalNumber: -1,
101 | optionalObject: {},
102 | optionalString: 'Hello, React',
103 | optionalEnum: 'News',
104 | };
105 | },
106 |
107 | render() {
108 | return null;
109 | }
110 | });
111 |
112 |
--------------------------------------------------------------------------------
/tests/js/fixtures/modern.js:
--------------------------------------------------------------------------------
1 |
2 | class ModernExample extends React.Component {
3 | render() {
4 | return null;
5 | }
6 | }
7 |
8 | ModernExample.propTypes = {
9 | optionalArray: React.PropTypes.array,
10 | optionalBool: React.PropTypes.bool,
11 | optionalFunc: React.PropTypes.func,
12 | optionalNumber: React.PropTypes.number,
13 | optionalObject: React.PropTypes.object,
14 | optionalString: React.PropTypes.string,
15 |
16 | // Anything that can be rendered: numbers, strings, elements or an array
17 | // containing these types.
18 | optionalNode: React.PropTypes.node,
19 |
20 | // A React element.
21 | optionalElement: React.PropTypes.element,
22 |
23 | // You can also declare that a prop is an instance of a class. This uses
24 | // JS's instanceof operator.
25 | optionalMessage: React.PropTypes.instanceOf(Message),
26 |
27 | // You can ensure that your prop is limited to specific values by treating
28 | // it as an enum.
29 | optionalEnum: React.PropTypes.oneOf(['News', 'Photos']),
30 |
31 | // An object that could be one of many types
32 | optionalUnion: React.PropTypes.oneOfType([
33 | React.PropTypes.string,
34 | React.PropTypes.number,
35 | React.PropTypes.instanceOf(Message)
36 | ]),
37 |
38 | // An array of a certain type
39 | optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number),
40 |
41 | // An object with property values of a certain type
42 | optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number),
43 |
44 | // An object taking on a particular shape
45 | optionalObjectWithShape: React.PropTypes.shape({
46 | color: React.PropTypes.string,
47 | fontSize: React.PropTypes.number
48 | }),
49 |
50 | // You can chain any of the above with `isRequired` to make sure a warning
51 | // is shown if the prop isn't provided.
52 | requiredFunc: React.PropTypes.func.isRequired,
53 |
54 | // A value of any data type
55 | requiredAny: React.PropTypes.any.isRequired,
56 |
57 | // You can also specify a custom validator. It should return an Error
58 | // object if the validation fails. Don't `console.warn` or throw, as this
59 | // won't work inside `oneOfType`.
60 | /*
61 | customProp: function(props, propName, componentName) {
62 | if (!/matchme/.test(props[propName])) {
63 | return new Error('Validation failed!');
64 | }
65 | }
66 | */
67 | };
68 |
69 | ModernExample.defaultProps = {
70 | optionalArray: [],
71 | optionalBool: false,
72 | optionalFunc: this.props.clickHandler,
73 | optionalNumber: -1,
74 | optionalObject: {},
75 | optionalString: 'Hello, React',
76 | optionalEnum: 'News',
77 | };
78 |
79 |
80 | class Round2 extends React.Component {
81 | render() {
82 | return null;
83 | }
84 | }
85 |
86 | Round2.propTypes = {
87 | optionalArray: React.PropTypes.array,
88 | };
89 |
90 | Round2.defaultProps = {
91 | optionalArray: [],
92 | optionalBool: false,
93 | optionalFunc: this.props.clickHandler,
94 | optionalNumber: -1,
95 | optionalObject: {},
96 | optionalString: 'Hello, React',
97 | optionalEnum: 'News',
98 | };
99 |
100 |
101 |
--------------------------------------------------------------------------------
/tests/js/integration.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var readFile = require('fs').readFileSync;
3 | var esprima = require('esprima-fb');
4 | var estraverse = require('estraverse-fb');
5 | var assert = require('assert');
6 |
7 | var results;
8 | var spyOnReturn = function(obj, method) {
9 | var orig = obj[method];
10 | results = [];
11 | obj[method] = function() {
12 | var result = orig.apply(obj, arguments);
13 | results.push(result);
14 | return result;
15 | }
16 | }
17 |
18 | module.exports = function(type) {
19 | spyOnReturn(require('../../transforms/annotationsFor'), 'extract');
20 |
21 | var contents = readFile(path.join(__dirname, 'fixtures', type + '.js'), 'utf8');
22 |
23 | var transformer = require('../../esprima-extractor');
24 |
25 | // TODO: add support for default values
26 | var fullExamplePropTypes = {
27 | optionalArray: {propType: 'array', defaultValue: '[]'},
28 | optionalBool: {propType: 'bool', defaultValue: 'false'},
29 | optionalFunc: {propType: 'func', defaultValue: 'this.props.clickHandler'},
30 | optionalNumber: {propType: 'number', defaultValue: -1},
31 | optionalObject: {propType: 'object', defaultValue: '{}'},
32 | optionalString: {propType: 'string', defaultValue: 'Hello, React'},
33 | optionalNode: {propType: 'node'},
34 | optionalElement: {propType: 'element'},
35 | optionalMessage: {propType: 'Message'},
36 | optionalEnum: {propType: ['News', 'Photos'], defaultValue: 'News'},
37 | optionalUnion: {propType: ['string', 'number', 'Message']},
38 | optionalArrayOf: {propType: 'number[]'},
39 | optionalObjectOf: {propType: 'number{}'},
40 | optionalObjectWithShape: {propType: {color: 'string', fontSize: 'number'}},
41 | requiredFunc: {propType: 'func', isRequired: true},
42 | requiredAny: {propType: 'any', isRequired: true},
43 | };
44 |
45 | var expected = {
46 | ClassicExample: fullExamplePropTypes,
47 | ModernExample: fullExamplePropTypes,
48 | Round2: {
49 | optionalArray: {propType: 'array', defaultValue: '[]'},
50 | optionalBool: {defaultValue: 'false'},
51 | optionalFunc: {defaultValue: 'this.props.clickHandler'},
52 | optionalNumber: {defaultValue: -1},
53 | optionalObject: {defaultValue: '{}'},
54 | optionalString: {defaultValue: 'Hello, React'},
55 | optionalEnum: {defaultValue: 'News'},
56 | },
57 | };
58 |
59 | transformer.onComplete = function(annotations) {
60 | Object.keys(annotations).forEach(function(displayName, i) {
61 | var result = annotations[displayName];
62 | var expect = expected[displayName];
63 |
64 | assert.deepEqual(
65 | result,
66 | expect,
67 | '\nExpectations for ' + displayName + ': \n' +
68 | JSON.stringify(result) +
69 | ' to be: \n' + JSON.stringify(expect)
70 | );
71 | });
72 | };
73 |
74 | var ast = estraverse[transformer.type](
75 | esprima.parse(contents),
76 | transformer
77 | );
78 |
79 | if (results.length === 0) {
80 | throw new Error(
81 | 'No results were parsed. Expected ' + expected.length + ' annotations.'
82 | );
83 | }
84 |
85 | };
86 |
87 |
--------------------------------------------------------------------------------
/tests/js/modern.js:
--------------------------------------------------------------------------------
1 | require('./integration')('modern');
2 |
3 |
--------------------------------------------------------------------------------
/transforms/annotationsFor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015, Skookum Digital Works, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | *
8 | * @providesModule annotationsFor
9 | */
10 | var oneOf = require('./types/oneOf');
11 |
12 | var annotators = require('./annotators');
13 | var defaulters = require('./defaulters');
14 |
15 | /**
16 | * Get the AST representation of a nodes propTypes definition
17 | */
18 | function _(type, node) {
19 | var target = node.value;
20 | var t = type[target.type] ? target.type : 'defaults'
21 | if (typeof type[t] !== 'function') {
22 | console.warn(
23 | 'Attempted to annotate unsupported node type ' + target.type
24 | );
25 | return null;
26 | }
27 |
28 | return type[t](target);
29 | }
30 |
31 | /**
32 | * Transform the AST back to it’s object representative.
33 | */
34 | function transform(value) {
35 | var type = typeof value;
36 | // primitive types are ready to go
37 | if (
38 | type === 'string' ||
39 | type === 'boolean' ||
40 | type === 'number'
41 | ) return value;
42 |
43 | if (value.type === 'Literal') {
44 | return value.value;
45 | }
46 |
47 | if (value.type === 'ArrayExpression') {
48 | return value.elements.map(function(e) {
49 | return transform(e.value);
50 | });
51 | }
52 |
53 | if (value.type === 'ObjectExpression') {
54 | return value.properties.reduce(function(o, k) {
55 | o[k.key.name] = transform(k.value);
56 | return o;
57 | }, {});
58 | }
59 | }
60 |
61 | module.exports = {
62 | // convert the AST back to it’s real JS object representation
63 | extract: function extract(node) {
64 | return (_(annotators, node) || []).reduce(function(o, k) {
65 | o[k.key] = transform(k.value);
66 | return o;
67 | }, {});
68 | },
69 |
70 | defaults: function defaultsFor(node) {
71 | return (_(defaulters, node)).reduce(function(o, k) {
72 | o[k.key] = transform(k.value);
73 | return o;
74 | }, {});
75 | },
76 |
77 | annotate: function annotationsFor(node) {
78 | var annotations = _(annotators, node);
79 | return {
80 | type: 'ObjectExpression',
81 | properties: annotations.map(function(a) {
82 | return {
83 | type: 'Property',
84 | key: {
85 | type: 'Identifier',
86 | name: a.key,
87 | },
88 | value: (typeof a.value === 'object') ?
89 | a.value :
90 | {
91 | type: 'Literal',
92 | value: a.value
93 | }
94 | };
95 | })
96 | };
97 | },
98 |
99 |
100 | defaultAnnotations: function defaultsFor(node) {
101 | var defaults = _(defaulters, node);
102 | return {
103 | type: 'ObjectExpression',
104 | properties: [{
105 | type: 'Property',
106 | key: {
107 | type: 'Identifier',
108 | name: 'defaultValue',
109 | },
110 | value: (typeof defaults.value === 'object') ?
111 | defaults.value : {
112 | type: 'Literal',
113 | value: defaults
114 | }
115 | }]
116 | };
117 | },
118 | };
119 |
120 |
--------------------------------------------------------------------------------
/transforms/annotators/CallExpression.js:
--------------------------------------------------------------------------------
1 | var PROP_TYPE_KEY = require('../propTypeKey');
2 | var types = require('../types');
3 |
4 | /**
5 | * Resolve annotations for call expressions
6 | *
7 | * @param {ASTNode} target
8 | * @return {Array} annotations
9 | */
10 | module.exports = function CallExpressionAnnotator(target) {
11 | var propType = target.callee.property.name;
12 | var propName = target.arguments[0].name;
13 |
14 | if (typeof types[propType] === 'undefined') {
15 | throw new Error('Attempted to annotate unknown CallExpression ' + propType);
16 | }
17 |
18 | var annotations = types[propType].resolve(target);
19 |
20 | return Array.isArray(annotations) ? annotations : [annotations];
21 | };
22 |
23 |
--------------------------------------------------------------------------------
/transforms/annotators/MemberExpression.js:
--------------------------------------------------------------------------------
1 | var PROP_TYPE_KEY = require('../propTypeKey');
2 |
3 | /**
4 | * Resolve annotations for member expressions
5 | *
6 | * @param {ASTNode} target
7 | * @return {Array} annotations
8 | */
9 | module.exports = function MemberExpressionAnnotator(target) {
10 | var annotations = [];
11 | var propName = target.property.name;
12 |
13 | // resolve the MemberExpression or CallExpression preceding the isRequired
14 | // React.PropTypes.type.isRequired
15 | // React.PropTypes.callExpressoin(arguments).isRequired
16 | if (propName === 'isRequired') {
17 | if (target.object) {
18 | annotations = annotations.concat(require('./' + target.object.type)(target.object));
19 | }
20 |
21 | annotations.push({
22 | key: propName,
23 | value: true,
24 | });
25 | }
26 | else {
27 | annotations.push({
28 | key: PROP_TYPE_KEY,
29 | value: propName,
30 | });
31 | }
32 |
33 | return annotations;
34 | };
35 |
36 |
--------------------------------------------------------------------------------
/transforms/annotators/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | CallExpression: require('./CallExpression'),
3 | MemberExpression: require('./MemberExpression'),
4 | };
5 |
6 |
--------------------------------------------------------------------------------
/transforms/defaulters/Literal.js:
--------------------------------------------------------------------------------
1 | var escodegen = require('escodegen');
2 |
3 | module.exports = function DefaultForLiteral(node) {
4 | return [
5 | {
6 | type: 'Literal',
7 | key: 'defaultValue',
8 | value: node.value.toString()
9 | }
10 | ];
11 | };
12 |
13 |
--------------------------------------------------------------------------------
/transforms/defaulters/_util.js:
--------------------------------------------------------------------------------
1 | var escodegen = require('escodegen');
2 |
3 | module.exports = {
4 | toCode: function(node) {
5 | return [
6 | {
7 | type: 'Literal',
8 | key: 'defaultValue',
9 | value: escodegen.generate(node)
10 | }
11 | ];
12 | }
13 | };
14 |
15 |
--------------------------------------------------------------------------------
/transforms/defaulters/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | Literal: require('./Literal'),
3 | defaults: require('./_util').toCode,
4 | };
5 |
6 |
--------------------------------------------------------------------------------
/transforms/prepend.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015, Skookum Digital Works, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | *
8 | * @providesModule prepend
9 | */
10 |
11 | var esprima = require('esprima-fb');
12 |
13 | var SOURCE_PREFIX = esprima.parse(
14 | '/* AUTODOC */\n' +
15 | 'var AnnotatePropTypes = require(' +
16 | JSON.stringify(require.resolve('./../annotate')) +
17 | ');'
18 | ).body;
19 |
20 | /**
21 | * Prepend the SOURCE_PREFIX to the AST program body.
22 | *
23 | * @param {Array} programBody the top level body property of an AST Program
24 | */
25 | module.exports = function prepend(programBody) {
26 | programBody.unshift(SOURCE_PREFIX[0]);
27 | };
28 |
29 |
--------------------------------------------------------------------------------
/transforms/propTypeKey.js:
--------------------------------------------------------------------------------
1 | module.exports = 'propType';
2 |
3 |
--------------------------------------------------------------------------------
/transforms/types/_util.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015, Skookum Digital Works, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | *
8 | * @providesModule _util
9 | */
10 |
11 | module.exports = {
12 | is: function(propTypeName) {
13 | return function() {
14 | return (
15 | typeof o === 'object' &&
16 | o.type === 'CallExpression' &&
17 | o.callee.property.name === propTypeName
18 | )
19 | }
20 | }
21 | };
22 |
23 |
--------------------------------------------------------------------------------
/transforms/types/arrayOf.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015, Skookum Digital Works, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | *
8 | * @providesModule arrayOf
9 | */
10 |
11 | var is = require('./_util').is;
12 | function value(o) { return o.value; }
13 |
14 | module.exports = {
15 | is: is('arrayOf'),
16 |
17 | // React.PropTypes.arrayOf(React.propTypes.string)
18 | // React.PropTypes.arrayOf(React.propTypes.number).isRequired
19 | resolve: function(o) {
20 | var result = require('../annotators/MemberExpression')(o.arguments[0])[0];
21 | result.value = result.value + '[]';
22 |
23 | return result;
24 | }
25 | };
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/transforms/types/index.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = {
3 | arrayOf: require('./arrayOf'),
4 | instanceOf: require('./instanceOf'),
5 | objectOf: require('./objectOf'),
6 | oneOf: require('./oneOf'),
7 | oneOfType: require('./oneOfType'),
8 | shape: require('./shape'),
9 | };
10 |
11 |
--------------------------------------------------------------------------------
/transforms/types/instanceOf.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015, Skookum Digital Works, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | *
8 | * @providesModule instanceOf
9 | */
10 |
11 | var is = require('./_util').is;
12 | function value(o) { return o.value; }
13 |
14 | module.exports = {
15 | is: is('instanceOf'),
16 |
17 | // React.PropTypes.instanceOf(Thing);
18 | resolve: function(o) {
19 | var value = o.arguments[0].name;
20 | return {
21 | key: 'propType',
22 | value: value,
23 | };
24 | }
25 | };
26 |
27 |
28 |
--------------------------------------------------------------------------------
/transforms/types/objectOf.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015, Skookum Digital Works, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | *
8 | * @providesModule objectOf
9 | */
10 |
11 | var is = require('./_util').is;
12 | function value(o) { return o.value; }
13 |
14 | module.exports = {
15 | is: is('arrayOf'),
16 |
17 | // React.PropTypes.objectOf(React.propTypes.string)
18 | // React.PropTypes.objectOf(React.propTypes.number).isRequired
19 | resolve: function(o) {
20 | var result = require('../annotators/MemberExpression')(o.arguments[0])[0];
21 | result.value = result.value + '{}';
22 |
23 | return result;
24 | }
25 | };
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/transforms/types/oneOf.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015, Skookum Digital Works, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | *
8 | * @providesModule oneOf
9 | */
10 |
11 | var is = require('./_util').is;
12 | function value(o) { return o.value; }
13 |
14 | module.exports = {
15 | is: is('oneOf'),
16 |
17 | // React.PropTypes.oneOf(Thing);
18 | resolve: function(o) {
19 | // TODO: if this is a variable reference instead of inline
20 | // we’ll need to look it up
21 | try {
22 | var elements = JSON.parse(JSON.stringify(o.arguments[0].elements));
23 | } catch(e) {
24 | // likely includes FunctionExpressions
25 | console.log(o.arguments[0].elements);
26 | }
27 | return {
28 | key: 'propType',
29 | value: {
30 | type: 'ArrayExpression',
31 | elements: elements,
32 | }
33 | };
34 | }
35 | };
36 |
37 |
--------------------------------------------------------------------------------
/transforms/types/oneOfType.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015, Skookum Digital Works, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | *
8 | * @providesModule oneOfType
9 | */
10 |
11 | var is = require('./_util').is;
12 | var getValue = function(o) { return o[0].value; }
13 |
14 | module.exports = {
15 | is: is('oneOfType'),
16 |
17 | // React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number])
18 | resolve: function(o) {
19 | // require these late to avoid circular dependency issues
20 | var annotators = require('../annotators/');
21 |
22 | var value = o.arguments[0];
23 | value = o.arguments[0].elements
24 | .map(function(node) {
25 | return annotators[node.type](node);
26 | })
27 | .map(getValue);
28 |
29 | return {
30 | key: 'propType',
31 | value: {
32 | type: 'ArrayExpression',
33 | elements: value.map(function(v) {
34 | return {type: 'Literal', value: v};
35 | })
36 | }
37 | };
38 | }
39 | };
40 |
41 |
--------------------------------------------------------------------------------
/transforms/types/shape.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015, Skookum Digital Works, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | *
8 | * @providesModule shape
9 | */
10 |
11 | var is = require('./_util').is;
12 | var MemberExpressionAnnotator = require('../annotators/MemberExpression');
13 | var getValue = function(o) { return o.value; }
14 |
15 | module.exports = {
16 | is: is('shape'),
17 |
18 | // React.PropTypes.shape({
19 | // name: React.PropTypes.string,
20 | // id: React.PropTypes.number
21 | // });
22 | // TODO: this could be dried up to use the core annotationsFor instead of
23 | // duplicating the effort
24 | resolve: function(o) {
25 | var value = o.arguments[0].properties
26 | .map(getValue)
27 | .map(MemberExpressionAnnotator)
28 | .map(function(c, i) {
29 | c[0].key = o.arguments[0].properties[i].key.name
30 | return c;
31 | });
32 |
33 | return {
34 | key: 'propType',
35 | value: {
36 | type: 'ObjectExpression',
37 | properties: value.map(function(v) {
38 | return {
39 | type: 'Property',
40 | key: {type: 'Identifier', name: v[0].key},
41 | value: {type: 'Literal', value: v[0].value}
42 | };
43 | })
44 | }
45 | };
46 | }
47 | };
48 |
49 |
--------------------------------------------------------------------------------