Autogenerate PropTypes from Flow types (#20)

Summary:
Closes #17; see discussion there.

This commit uses the `babel-plugin-flow-react-proptypes` package to
automatically create PropType definitions from components that are typed
with Flow. It simultaneously updates all of our existing components to
be typed with Flow. As a result, we have both static and dynamic type
checking.

Test Plan:
Note that `yarn test` and `yarn flow` report no errors, and that there
are no prop validation errors at runtime with `yarn start`.

Then, apply the following patch:
```diff
diff --git a/explorer/src/UserExplorer.js b/explorer/src/UserExplorer.js
index bb574cd..636a10d 100644
--- a/explorer/src/UserExplorer.js
+++ b/explorer/src/UserExplorer.js
@@ -18,7 +18,7 @@ export class UserExplorer extends Component<{
         .sort((a,b) => b[1] - a[1]);
     const entries = sortedUserWeightTuples.map(authorWeight => {
       const [author, weight] = authorWeight;
-      return <UserEntry userId={author} weight={weight} key={author}/>
+      return <UserEntry userId={55} weight={weight} key={author}/>
     });
     return <div className="user-explorer">
       <h3> User Explorer </h3>
```
Note that `yarn test` fails (the `App.test.js` E2E rendering test),
`yarn flow` fails, and there is a runtime prop validation error.

wchargin-branch: autogenerate-proptypes
This commit is contained in:
William Chargin 2018-02-17 13:30:16 -08:00 committed by GitHub
parent ee59eb9b30
commit 5744d3c860
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 57 additions and 65 deletions

View File

@ -85,11 +85,17 @@
] ]
}, },
"babel": { "babel": {
"plugins": [
"flow-react-proptypes"
],
"presets": [ "presets": [
"react-app" "react-app"
] ]
}, },
"eslintConfig": { "eslintConfig": {
"extends": "react-app" "extends": "react-app"
},
"devDependencies": {
"babel-plugin-flow-react-proptypes": "^17.1.2"
} }
} }

View File

@ -1,10 +1,12 @@
// @flow
import React, { Component } from 'react'; import React, { Component } from 'react';
import data from './data.json'; import data from './data.json';
import './App.css'; import './App.css';
import { FileExplorer } from './FileExplorer.js'; import { FileExplorer } from './FileExplorer.js';
import { UserExplorer } from './UserExplorer.js'; import { UserExplorer } from './UserExplorer.js';
class App extends Component { type AppState = {selectedPath: string, selectedUser: ?string};
class App extends Component<{}, AppState> {
constructor() { constructor() {
super(); super();
this.state = { this.state = {

View File

@ -1,15 +1,13 @@
// @flow
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {buildTree} from './commitUtils'; import {buildTree} from './commitUtils';
import {propTypes as commitUtilsPropTypes} from './commitUtils'; import type {CommitData, FileTree} from './commitUtils';
export class FileExplorer extends Component {
static propTypes = {
selectedPath: PropTypes.string,
onSelectPath: PropTypes.func.isRequired,
data: commitUtilsPropTypes.commitData.isRequired,
}
export class FileExplorer extends Component<{
selectedPath: string,
onSelectPath: (newPath: string) => void,
data: CommitData,
}> {
render() { render() {
// within the FileExplorer, paths start with "./", outside they don't // within the FileExplorer, paths start with "./", outside they don't
// which is hacky and should be cleaned up // which is hacky and should be cleaned up
@ -38,19 +36,16 @@ export class FileExplorer extends Component {
} }
} }
class FileEntry extends Component { class FileEntry extends Component<{
static propTypes = { name: string,
name: PropTypes.string.isRequired, path: string,
path: PropTypes.string.isRequired, alwaysExpand: bool,
alwaysExpand: PropTypes.bool.isRequired, tree: FileTree,
selectedPath: string,
// The type for the tree is recursive, and is annoying to specify as onSelectPath: (newPath: string) => void,
// a proptype. The Flow type definition is in commitUtils.js. }, {
tree: PropTypes.object.isRequired, expanded: bool,
}> {
selectedPath: PropTypes.string.isRequired,
onSelectPath: PropTypes.func.isRequired,
}
constructor() { constructor() {
super(); super();
@ -89,4 +84,5 @@ class FileEntry extends Component {
{(this.state.expanded || this.props.alwaysExpand) && subEntries} {(this.state.expanded || this.props.alwaysExpand) && subEntries}
</div> </div>
} }
} }

View File

@ -1,18 +1,14 @@
// @flow
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import {commitWeight, userWeightForPath} from './commitUtils';
import { import type {CommitData, FileTree} from './commitUtils';
commitWeight,
propTypes as commitUtilsPropTypes,
userWeightForPath,
} from './commitUtils';
export class UserExplorer extends Component { export class UserExplorer extends Component<{
static propTypes = { selectedPath: string,
selectedPath: PropTypes.string.isRequired, selectedUser: ?string,
selectedUser: PropTypes.string, onSelectUser: (newUser: string) => void,
onSelectUser: PropTypes.func.isRequired, data: CommitData,
data: commitUtilsPropTypes.commitData.isRequired, }> {
}
render() { render() {
const weights = userWeightForPath(this.props.selectedPath, this.props.data, commitWeight); const weights = userWeightForPath(this.props.selectedPath, this.props.data, commitWeight);
@ -29,16 +25,16 @@ export class UserExplorer extends Component {
{entries} {entries}
</div> </div>
} }
} }
/** /**
* Record the cred earned by the user in a given scope. * Record the cred earned by the user in a given scope.
*/ */
class UserEntry extends Component { class UserEntry extends Component<{
static propTypes = { userId: string,
userId: PropTypes.string.isRequired, weight: number,
weight: PropTypes.number.isRequired, }> {
}
render() { render() {
return <div className="user-entry"> return <div className="user-entry">
@ -46,4 +42,5 @@ class UserEntry extends Component {
<span> {this.props.weight.toFixed(1)} </span> <span> {this.props.weight.toFixed(1)} </span>
</div> </div>
} }
} }

View File

@ -2,30 +2,12 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
type CommitData = { export type CommitData = {
// TODO improve variable names // TODO improve variable names
fileToCommits: {[filename: string]: string[]}; fileToCommits: {[filename: string]: string[]};
commits: {[commithash: string]: Commit}; commits: {[commithash: string]: Commit};
authors: string[];
} }
export const propTypes = {
commitData: PropTypes.shape({
fileToCommits: PropTypes.objectOf(
PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
).isRequired,
commits: PropTypes.objectOf(PropTypes.shape({
author: PropTypes.string.isRequired,
stats: PropTypes.objectOf(PropTypes.shape({
lines: PropTypes.number.isRequired,
insertions: PropTypes.number.isRequired,
deletions: PropTypes.number.isRequired,
}).isRequired).isRequired,
}).isRequired).isRequired,
}),
};
type Commit = { type Commit = {
author: string; author: string;
stats: {[filename: string]: FileStats}; stats: {[filename: string]: FileStats};
@ -86,7 +68,7 @@ export function userWeightForPath(path: string, data: CommitData, weightFn: Weig
return userWeightMap; return userWeightMap;
} }
type FileTree = {[string]: FileTree}; export type FileTree = {[string]: FileTree};
export function buildTree(fileNames: string[]): FileTree { export function buildTree(fileNames: string[]): FileTree {
const sortedFileNames = fileNames.slice().sort(); const sortedFileNames = fileNames.slice().sort();

View File

@ -323,7 +323,7 @@ babel-code-frame@6.26.0, babel-code-frame@^6.11.0, babel-code-frame@^6.22.0, bab
esutils "^2.0.2" esutils "^2.0.2"
js-tokens "^3.0.2" js-tokens "^3.0.2"
babel-core@6.26.0, babel-core@^6.0.0, babel-core@^6.26.0: babel-core@6.26.0, babel-core@^6.0.0, babel-core@^6.25.0, babel-core@^6.26.0:
version "6.26.0" version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8"
dependencies: dependencies:
@ -514,6 +514,15 @@ babel-plugin-dynamic-import-node@1.1.0:
babel-template "^6.26.0" babel-template "^6.26.0"
babel-types "^6.26.0" babel-types "^6.26.0"
babel-plugin-flow-react-proptypes@^17.1.2:
version "17.1.2"
resolved "https://registry.yarnpkg.com/babel-plugin-flow-react-proptypes/-/babel-plugin-flow-react-proptypes-17.1.2.tgz#89f75928a47ea869dab312605f42542dd7b6755c"
dependencies:
babel-core "^6.25.0"
babel-template "^6.25.0"
babel-traverse "^6.25.0"
babel-types "^6.25.0"
babel-plugin-istanbul@^4.0.0: babel-plugin-istanbul@^4.0.0:
version "4.1.5" version "4.1.5"
resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.5.tgz#6760cdd977f411d3e175bb064f2bc327d99b2b6e" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.5.tgz#6760cdd977f411d3e175bb064f2bc327d99b2b6e"
@ -913,7 +922,7 @@ babel-runtime@6.26.0, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtim
core-js "^2.4.0" core-js "^2.4.0"
regenerator-runtime "^0.11.0" regenerator-runtime "^0.11.0"
babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0: babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.25.0, babel-template@^6.26.0:
version "6.26.0" version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
dependencies: dependencies:
@ -923,7 +932,7 @@ babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0:
babylon "^6.18.0" babylon "^6.18.0"
lodash "^4.17.4" lodash "^4.17.4"
babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-traverse@^6.26.0: babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-traverse@^6.25.0, babel-traverse@^6.26.0:
version "6.26.0" version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
dependencies: dependencies:
@ -937,7 +946,7 @@ babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-tr
invariant "^2.2.2" invariant "^2.2.2"
lodash "^4.17.4" lodash "^4.17.4"
babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.26.0: babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.25.0, babel-types@^6.26.0:
version "6.26.0" version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
dependencies: dependencies: