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:
parent
ee59eb9b30
commit
5744d3c860
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue