├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── __tests__ ├── __snapshots__ │ └── proptypes-to-flow-test.js.snap └── proptypes-to-flow-test.js ├── package.json ├── src ├── helpers │ ├── ReactUtils.js │ ├── annotateConstructor.js │ ├── createTypeAlias.js │ ├── findIndex.js │ ├── findParentBody.js │ ├── propTypeToFlowType.js │ ├── removePropTypeImport.js │ └── transformProperties.js ├── index.js └── transformers │ ├── es6Classes.js │ └── functional.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'airbnb-base', 4 | 'prettier', 5 | ], 6 | env: { 7 | 'node': true, 8 | }, 9 | root: true, 10 | plugins: [ 11 | 'prettier', 12 | ], 13 | rules: { 14 | 'comma-dangle': ['error', 'always-multiline'], 15 | 'import/order': ['error', { 16 | 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 17 | }], 18 | 'indent': ['off', 2, {'SwitchCase': 1}], 19 | 'max-len': ['error', 120, 4, {'ignoreComments': true, 'ignoreUrls': true}], 20 | 'no-unused-vars': ['error', {'vars': 'all', 'args': 'none'}], 21 | 'space-before-function-paren': ['error', 'never'], 22 | 'space-in-parens': ['error', 'never'], 23 | 'no-underscore-dangle': 'off', 24 | 'no-param-reassign': 'off', 25 | 'no-console': 'off', 26 | 'no-warning-comments': ['warn', { 'terms': ['fixme'], 'location': 'start' }], 27 | 28 | 'prettier/prettier': ['error', {'trailingComma': 'es5', 'singleQuote': true} ], 29 | 30 | // NOTE: Disabled to not do too many changes to original codebase: 31 | 'consistent-return': 'off', 32 | 'arrow-body-style': 'off', 33 | 'object-curly-spacing': 'off', 34 | 'padded-blocks': 'off', 35 | 'arrow-parens': 'off', 36 | 'import/extensions': 'off', 37 | 'no-shadow': 'off', 38 | 'array-callback-return': 'off', 39 | 'no-else-return': 'off', 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log* 3 | lib 4 | .DS_Store 5 | coverage 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '7' 4 | - '6' 5 | - '4' 6 | 7 | before_install: 8 | - npm install -g codecov 9 | 10 | script: 11 | - yarn run check 12 | - yarn run build 13 | - codecov 14 | 15 | cache: yarn 16 | 17 | after_success: 18 | - bash <(curl -s https://codecov.io/bash) -f coverage/coverage-final.json 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Billy Vong 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # codemod-proptypes-to-flow [![Build Status](https://travis-ci.org/billyvg/codemod-proptypes-to-flow.svg?branch=master)](https://travis-ci.org/billyvg/codemod-proptypes-to-flow) [![codecov](https://codecov.io/gh/billyvg/codemod-proptypes-to-flow/branch/master/graph/badge.svg)](https://codecov.io/gh/billyvg/codemod-proptypes-to-flow) 2 | 3 | Removes `React.PropTypes` and attempts to transform to [Flow](http://flow.org/). 4 | 5 | ### Setup & Run 6 | * `npm install -g jscodeshift` 7 | * `git clone https://github.com/billyvg/codemod-proptypes-to-flow` 8 | * `jscodeshift -t codemod-proptypes-to-flow/src/index.js ` 9 | * Use the `-d` option for a dry-run and use `-p` to print the output 10 | for comparison 11 | 12 | #### Options 13 | Behavior of this codemod can be customized by passing options to jscodeshift e.g.: 14 | ``` 15 | jscodeshift -t codemod-proptypes-to-flow/src/index.js --flowComment=line 16 | ``` 17 | 18 | Following options are accepted: 19 | 20 | ##### flowComment 21 | `--flowComment=` - type of flow comment. Defaults to `block`. 22 | 23 | ``` 24 | --flowComment=block: /* @flow */ 25 | --flowComment=line: // @flow 26 | ``` 27 | 28 | ##### propsTypeSuffix 29 | `--propsTypeSuffix=` - used to customize the type names generated by the codemod. Provided string will be used alone or appended to Component's name when defining props type. Defaults to `Props`. 30 | 31 | Default: 32 | ``` 33 | type Props = {...} 34 | type MyComponentProps = {...} 35 | ``` 36 | 37 | With `--propsTypeSuffix=PropsType`: 38 | ``` 39 | type PropsType = {...} 40 | type MyComponentPropsType = {...} 41 | ``` 42 | 43 | ### Not working/Implemented yet 44 | * Custom validators 45 | * `React.createClass` 46 | * Use of importing PropTypes 47 | 48 | ### Contributors 49 | * Thanks to [@skovhus](https://github.com/skovhus) for adding support for functional components and modernizing the codebase a bit (a lot) 50 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/proptypes-to-flow-test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`React.PropTypes to flow add empty PropTypes (no constructor) 1`] = ` 4 | " 5 | /* @flow */ 6 | import React from 'react'; 7 | import { View } from 'react-native'; 8 | 9 | type Props = {}; 10 | 11 | class Cards extends React.Component { 12 | props: Props; 13 | render() { 14 | return ( 15 | 16 | ); 17 | } 18 | } 19 | 20 | export default Cards; 21 | " 22 | `; 23 | 24 | exports[`React.PropTypes to flow adds empty PropTypes (constructor) 1`] = ` 25 | " 26 | /* @flow */ 27 | import { Component } from 'react'; 28 | import { View } from 'react-native'; 29 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 30 | 31 | type Props = {}; 32 | 33 | class PureComponent extends Component { 34 | constructor(props: Props) { 35 | super(props); 36 | 37 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 38 | } 39 | props: Props; 40 | render() { 41 | return ( 42 | 43 | ); 44 | } 45 | } 46 | 47 | export default PureComponent; 48 | " 49 | `; 50 | 51 | exports[`React.PropTypes to flow adds type annotation to \`prop\` parameter in constructor (ES2015) 1`] = ` 52 | " 53 | /* @flow */ 54 | import React from 'react'; 55 | 56 | type ComponentProps = {}; 57 | 58 | export default class Component extends React.Component { 59 | constructor(props: ComponentProps) { 60 | super(props); 61 | } 62 | 63 | props: ComponentProps; 64 | 65 | componentDidMount() { 66 | } 67 | } 68 | 69 | type Component2Props = {}; 70 | 71 | class Component2 extends React.Component { 72 | constructor(props: Component2Props) { 73 | super(props); 74 | } 75 | 76 | props: Component2Props; 77 | 78 | componentDidMount() { 79 | } 80 | } 81 | " 82 | `; 83 | 84 | exports[`React.PropTypes to flow does not touch files with flow Props already declared 1`] = ` 85 | " 86 | /* @flow */ 87 | import React from 'react'; 88 | 89 | export type Props = { 90 | created_at?: string, 91 | }; 92 | 93 | class MyComponent extends React.Component { 94 | props: Props; 95 | 96 | render() { 97 | return ( 98 |
99 | ); 100 | } 101 | } 102 | 103 | export default MyComponent; 104 | " 105 | `; 106 | 107 | exports[`React.PropTypes to flow does not touch non React classes 1`] = ` 108 | " 109 | class PureComponent extends Class { 110 | constructor() { 111 | } 112 | } 113 | 114 | export default PureComponent; 115 | " 116 | `; 117 | 118 | exports[`React.PropTypes to flow handles block comments 1`] = ` 119 | " 120 | /* @flow */ 121 | import React from 'react'; 122 | 123 | type Props = { 124 | /** 125 | * block comment 126 | */ 127 | optionalArray?: Array, 128 | anotherProp?: string, 129 | }; 130 | 131 | export default class Test extends React.Component { 132 | props: Props; 133 | } 134 | " 135 | `; 136 | 137 | exports[`React.PropTypes to flow handles functional components with expression body 1`] = ` 138 | " 139 | /* @flow */ 140 | import React from 'react'; 141 | export type Props = { hello: string }; 142 | const MyComponent = (props: Props) => { 143 | const { hello } = props; 144 | return
{hello}
; 145 | }; 146 | export default MyComponent; 147 | " 148 | `; 149 | 150 | exports[`React.PropTypes to flow handles presence of defaultProps 1`] = ` 151 | " 152 | /* @flow */ 153 | import React from 'react'; 154 | 155 | type Props = { 156 | /** 157 | * block comment 158 | */ 159 | optionalArray?: Array, 160 | anotherProp?: string, 161 | }; 162 | 163 | export default class Test extends React.Component { 164 | props: Props; 165 | 166 | static defaultProps = { 167 | anotherProp: '' 168 | }; 169 | } 170 | " 171 | `; 172 | 173 | exports[`React.PropTypes to flow preserves comments 1`] = ` 174 | " 175 | /* @flow */ 176 | import React from 'react'; 177 | 178 | export type Props = { 179 | // You can declare that a prop is a specific JS primitive. By default, these 180 | // are all optional. 181 | optionalArray?: Array, 182 | optionalBool?: boolean, 183 | optionalFunc?: Function, 184 | optionalNumber?: number, 185 | optionalObject?: Object, 186 | optionalString?: string, 187 | // Anything that can be rendered: numbers, strings, elements or an array 188 | // (or fragment) containing these types. 189 | optionalNode?: number | string | React.Element | Array, 190 | // A React element. 191 | optionalElement?: React.Element, 192 | // You can also declare that a prop is an instance of a class. This uses 193 | // JS's instanceof operator. 194 | optionalMessage?: Message, 195 | // You can ensure that your prop is limited to specific values by treating 196 | // it as an enum. 197 | optionalEnum?: 'News' | 'Photos', 198 | // An object that could be one of many types 199 | optionalUnion?: string | number | Message, 200 | // An array of a certain type 201 | optionalArrayOf?: Array, 202 | // An object with property values of a certain type 203 | optionalObjectOf?: Object, 204 | // An object taking on a particular shape 205 | optionalObjectWithShape?: { 206 | color?: string, 207 | fontSize?: number, 208 | }, 209 | // You can chain any of the above with \`isRequired\` to make sure a warning 210 | // is shown if the prop isn't provided. 211 | requiredFunc: Function, 212 | // A value of any data type 213 | requiredAny: any, 214 | }; 215 | 216 | function Button(props: Props) { 217 | return ( 218 | 221 | ); 222 | } 223 | " 224 | `; 225 | 226 | exports[`React.PropTypes to flow removes react's 16 PropTypes import 1`] = ` 227 | " 228 | /* @flow */ 229 | import React from 'react'; 230 | 231 | export type Props = { 232 | optionalArray?: Array, 233 | optionalBool?: boolean, 234 | }; 235 | 236 | function Button(props: Props) { 237 | return ( 238 | 241 | ); 242 | } 243 | " 244 | `; 245 | 246 | exports[`React.PropTypes to flow removes react's 16 destructured PropTypes import 1`] = ` 247 | " 248 | /* @flow */ 249 | import React from 'react'; 250 | export type Props = { optionalArray?: Array }; 251 | 252 | function Button(props: Props) { 253 | return ( 254 | 257 | ); 258 | } 259 | " 260 | `; 261 | 262 | exports[`React.PropTypes to flow transforms PropTypes that are a class property 1`] = ` 263 | " 264 | /* @flow */ 265 | import React from 'react'; 266 | 267 | type Props = { 268 | optionalArray?: Array, 269 | optionalBool?: boolean, 270 | optionalFunc?: Function, 271 | optionalNumber?: number, 272 | optionalObject?: Object, 273 | optionalString?: string, 274 | optionalNode?: number | string | React.Element | Array, 275 | optionalElement?: React.Element, 276 | optionalMessage?: Message, 277 | optionalEnum?: 'News' | 'Photos', 278 | optionalUnion?: string | number | Message, 279 | optionalArrayOf?: Array, 280 | optionalObjectOf?: Object, 281 | optionalObjectWithShape?: { 282 | color?: string, 283 | fontSize?: number, 284 | }, 285 | requiredFunc: Function, 286 | requiredAny: any, 287 | }; 288 | 289 | export default class Test extends React.Component { 290 | constructor(props: Props) { 291 | super(props); 292 | } 293 | props: Props; 294 | } 295 | " 296 | `; 297 | 298 | exports[`React.PropTypes to flow transforms PropTypes that are defined outside of class definition 1`] = ` 299 | " 300 | /* @flow */ 301 | import React from 'react'; 302 | 303 | type Props = { 304 | optionalArray?: Array, 305 | optionalBool?: boolean, 306 | optionalFunc?: Function, 307 | optionalNumber?: number, 308 | optionalObject?: Object, 309 | optionalString?: string, 310 | optionalNode?: number | string | React.Element | Array, 311 | optionalElement?: React.Element, 312 | optionalMessage?: Message, 313 | optionalEnum?: 'News' | 'Photos', 314 | optionalUnion?: string | number | Message, 315 | optionalArrayOf?: Array, 316 | optionalObjectOf?: Object, 317 | optionalObjectWithShape?: { 318 | color?: string, 319 | fontSize?: number, 320 | }, 321 | requiredFunc: Function, 322 | requiredAny: any, 323 | }; 324 | 325 | export default class Test extends React.Component { 326 | props: Props; 327 | componentDidMount() { 328 | } 329 | } 330 | " 331 | `; 332 | 333 | exports[`React.PropTypes to flow transforms optional PropTypes prefixed with \`React\` 1`] = ` 334 | " 335 | /* @flow */ 336 | import React from 'react'; 337 | 338 | export type Props = { 339 | optionalArray?: Array, 340 | optionalBool?: boolean, 341 | optionalFunc?: Function, 342 | optionalNumber?: number, 343 | optionalObject?: Object, 344 | optionalString?: string, 345 | optionalNode?: number | string | React.Element | Array, 346 | optionalElement?: React.Element, 347 | optionalMessage?: Message, 348 | optionalEnum?: 'News' | 'Photos', 349 | optionalUnion?: string | number | Message, 350 | optionalArrayOf?: Array, 351 | optionalObjectOf?: Object, 352 | optionalObjectWithShape?: { 353 | color?: string, 354 | fontSize?: number, 355 | }, 356 | }; 357 | 358 | export const F = (props: Props) => 359 |
; 360 | " 361 | `; 362 | 363 | exports[`React.PropTypes to flow transforms optional PropTypes with no \`React\` prefix 1`] = ` 364 | " 365 | /* @flow */ 366 | import React from 'react'; 367 | 368 | export type Props = { 369 | optionalArray?: Array, 370 | optionalBool?: boolean, 371 | optionalFunc?: Function, 372 | optionalNumber?: number, 373 | optionalObject?: Object, 374 | optionalString?: string, 375 | optionalNode?: number | string | React.Element | Array, 376 | optionalElement?: React.Element, 377 | optionalMessage?: Message, 378 | optionalEnum?: 'News' | 'Photos', 379 | optionalUnion?: string | number | Message, 380 | optionalArrayOf?: Array, 381 | optionalObjectOf?: Object, 382 | optionalObjectWithShape?: { 383 | color?: string, 384 | fontSize?: number, 385 | }, 386 | }; 387 | 388 | function Button(props: Props) { 389 | return ( 390 | 393 | ); 394 | } 395 | " 396 | `; 397 | 398 | exports[`React.PropTypes to flow transforms required PropTypes prefixed with \`React\` 1`] = ` 399 | " 400 | /* @flow */ 401 | /* eslint */ 402 | import React from 'react'; 403 | 404 | export type ButtonProps = { 405 | requiredArray: Array, 406 | requiredBool: boolean, 407 | requiredFunc: Function, 408 | requiredNumber: number, 409 | requiredObject: Object, 410 | requiredString: string, 411 | requiredNode: number | string | React.Element | Array, 412 | requiredElement: React.Element, 413 | requiredMessage: Message, 414 | requiredEnum: 'News' | 'Photos', 415 | requiredUnion: string | number | Message, 416 | requiredArrayOf: Array, 417 | requiredObjectOf: Object, 418 | requiredObjectWithShape: { 419 | color: string, 420 | fontSize: number, 421 | }, 422 | }; 423 | 424 | function Button(props: ButtonProps) { 425 | return ( 426 | 429 | ); 430 | } 431 | 432 | export type Button2Props = { requiredArray: Array }; 433 | 434 | function Button2(props: Button2Props) { 435 | const { requiredArray } = props; 436 | return ( 437 | 440 | ); 441 | } 442 | " 443 | `; 444 | 445 | exports[`React.PropTypes to flow transforms required PropTypes with no \`React\` prefix 1`] = ` 446 | " 447 | /* @flow */ 448 | import React from 'react'; 449 | 450 | export type Props = { 451 | requiredArray: Array, 452 | requiredBool: boolean, 453 | requiredFunc: Function, 454 | requiredNumber: number, 455 | requiredObject: Object, 456 | requiredString: string, 457 | requiredNode: number | string | React.Element | Array, 458 | requiredElement: React.Element, 459 | requiredMessage: Message, 460 | requiredEnum: 'News' | 'Photos', 461 | requiredUnion: string | number | Message, 462 | requiredArrayOf: Array, 463 | requiredObjectOf: Object, 464 | requiredObjectWithShape: { 465 | color: string, 466 | fontSize: number, 467 | }, 468 | }; 469 | 470 | export function Button(props: Props) { 471 | return ( 472 | 475 | ); 476 | } 477 | " 478 | `; 479 | 480 | exports[`React.PropTypes to flow transforms something that just looks like React class 1`] = ` 481 | " 482 | /* @flow */ 483 | import React from 'react'; 484 | import PureComponent from '../PureComponent'; 485 | 486 | type Props = { optionalArray?: Array }; 487 | 488 | class Test extends PureComponent { 489 | props: Props; 490 | render() { 491 | return ( 492 |
493 | ); 494 | } 495 | } 496 | 497 | export default Test; 498 | " 499 | `; 500 | -------------------------------------------------------------------------------- /__tests__/proptypes-to-flow-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const jscodeshift = require('jscodeshift'); 3 | 4 | const transform = require('../src/index').default; 5 | 6 | const transformString = (source, path = 'test.js') => { 7 | return transform({ path, source }, { jscodeshift }, {}); 8 | }; 9 | 10 | describe('React.PropTypes to flow', () => { 11 | it('transforms optional PropTypes prefixed with `React`', () => { 12 | const input = ` 13 | import React from 'react'; 14 | 15 | export const F = (props) => 16 |
; 17 | 18 | F.propTypes = { 19 | optionalArray: React.PropTypes.array, 20 | optionalBool: React.PropTypes.bool, 21 | optionalFunc: React.PropTypes.func, 22 | optionalNumber: React.PropTypes.number, 23 | optionalObject: React.PropTypes.object, 24 | optionalString: React.PropTypes.string, 25 | optionalNode: React.PropTypes.node, 26 | optionalElement: React.PropTypes.element, 27 | optionalMessage: React.PropTypes.instanceOf(Message), 28 | optionalEnum: React.PropTypes.oneOf(['News', 'Photos']), 29 | optionalUnion: React.PropTypes.oneOfType([ 30 | React.PropTypes.string, 31 | React.PropTypes.number, 32 | React.PropTypes.instanceOf(Message) 33 | ]), 34 | optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number), 35 | optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number), 36 | optionalObjectWithShape: React.PropTypes.shape({ 37 | color: React.PropTypes.string, 38 | fontSize: React.PropTypes.number 39 | }), 40 | }; 41 | `; 42 | 43 | expect(transformString(input)).toMatchSnapshot(); 44 | }); 45 | 46 | it('transforms required PropTypes prefixed with `React`', () => { 47 | const input = ` 48 | /* eslint */ 49 | import React from 'react'; 50 | 51 | function Button(props) { 52 | return ( 53 | 56 | ); 57 | } 58 | 59 | Button.propTypes = { 60 | requiredArray: React.PropTypes.array.isRequired, 61 | requiredBool: React.PropTypes.bool.isRequired, 62 | requiredFunc: React.PropTypes.func.isRequired, 63 | requiredNumber: React.PropTypes.number.isRequired, 64 | requiredObject: React.PropTypes.object.isRequired, 65 | requiredString: React.PropTypes.string.isRequired, 66 | requiredNode: React.PropTypes.node.isRequired, 67 | requiredElement: React.PropTypes.element.isRequired, 68 | requiredMessage: React.PropTypes.instanceOf(Message).isRequired, 69 | requiredEnum: React.PropTypes.oneOf(['News', 'Photos']).isRequired, 70 | requiredUnion: React.PropTypes.oneOfType([ 71 | React.PropTypes.string, 72 | React.PropTypes.number, 73 | React.PropTypes.instanceOf(Message) 74 | ]).isRequired, 75 | requiredArrayOf: React.PropTypes.arrayOf(React.PropTypes.number).isRequired, 76 | requiredObjectOf: React.PropTypes.objectOf(React.PropTypes.number).isRequired, 77 | requiredObjectWithShape: React.PropTypes.shape({ 78 | color: React.PropTypes.string.isRequired, 79 | fontSize: React.PropTypes.number.isRequired, 80 | }).isRequired, 81 | }; 82 | 83 | function Button2({ requiredArray }) { 84 | return ( 85 | 88 | ); 89 | } 90 | 91 | Button2.propTypes = { 92 | requiredArray: React.PropTypes.array.isRequired, 93 | }; 94 | `; 95 | 96 | expect(transformString(input)).toMatchSnapshot(); 97 | }); 98 | 99 | it('transforms optional PropTypes with no `React` prefix', () => { 100 | const input = ` 101 | import React, { PropTypes } from 'react'; 102 | 103 | function Button(props) { 104 | return ( 105 | 108 | ); 109 | } 110 | 111 | Button.propTypes = { 112 | optionalArray: PropTypes.array, 113 | optionalBool: PropTypes.bool, 114 | optionalFunc: PropTypes.func, 115 | optionalNumber: PropTypes.number, 116 | optionalObject: PropTypes.object, 117 | optionalString: PropTypes.string, 118 | optionalNode: PropTypes.node, 119 | optionalElement: PropTypes.element, 120 | optionalMessage: PropTypes.instanceOf(Message), 121 | optionalEnum: PropTypes.oneOf(['News', 'Photos']), 122 | optionalUnion: PropTypes.oneOfType([ 123 | PropTypes.string, 124 | PropTypes.number, 125 | PropTypes.instanceOf(Message) 126 | ]), 127 | optionalArrayOf: PropTypes.arrayOf(PropTypes.number), 128 | optionalObjectOf: PropTypes.objectOf(PropTypes.number), 129 | optionalObjectWithShape: PropTypes.shape({ 130 | color: PropTypes.string, 131 | fontSize: PropTypes.number 132 | }), 133 | }; 134 | `; 135 | 136 | expect(transformString(input)).toMatchSnapshot(); 137 | }); 138 | 139 | it("removes react's 16 PropTypes import", () => { 140 | const input = ` 141 | import React from 'react'; 142 | import PropTypes from 'prop-types'; 143 | 144 | function Button(props) { 145 | return ( 146 | 149 | ); 150 | } 151 | 152 | Button.propTypes = { 153 | optionalArray: PropTypes.array, 154 | optionalBool: PropTypes.bool, 155 | }; 156 | `; 157 | 158 | expect(transformString(input)).toMatchSnapshot(); 159 | }); 160 | 161 | it("removes react's 16 destructured PropTypes import", () => { 162 | const input = ` 163 | import React from 'react'; 164 | import { bool, array } from 'prop-types'; 165 | 166 | function Button(props) { 167 | return ( 168 | 171 | ); 172 | } 173 | 174 | Button.propTypes = { 175 | optionalArray: PropTypes.array, 176 | }; 177 | `; 178 | 179 | expect(transformString(input)).toMatchSnapshot(); 180 | }); 181 | 182 | it('transforms required PropTypes with no `React` prefix', () => { 183 | const input = ` 184 | import React, { PropTypes } from 'react'; 185 | 186 | export function Button(props) { 187 | return ( 188 | 191 | ); 192 | } 193 | 194 | Button.propTypes = { 195 | requiredArray: PropTypes.array.isRequired, 196 | requiredBool: PropTypes.bool.isRequired, 197 | requiredFunc: PropTypes.func.isRequired, 198 | requiredNumber: PropTypes.number.isRequired, 199 | requiredObject: PropTypes.object.isRequired, 200 | requiredString: PropTypes.string.isRequired, 201 | requiredNode: PropTypes.node.isRequired, 202 | requiredElement: PropTypes.element.isRequired, 203 | requiredMessage: PropTypes.instanceOf(Message).isRequired, 204 | requiredEnum: PropTypes.oneOf(['News', 'Photos']).isRequired, 205 | requiredUnion: PropTypes.oneOfType([ 206 | PropTypes.string, 207 | PropTypes.number, 208 | PropTypes.instanceOf(Message) 209 | ]).isRequired, 210 | requiredArrayOf: PropTypes.arrayOf(PropTypes.number).isRequired, 211 | requiredObjectOf: PropTypes.objectOf(PropTypes.number).isRequired, 212 | requiredObjectWithShape: PropTypes.shape({ 213 | color: PropTypes.string.isRequired, 214 | fontSize: PropTypes.number.isRequired, 215 | }).isRequired, 216 | }; 217 | `; 218 | 219 | expect(transformString(input)).toMatchSnapshot(); 220 | }); 221 | 222 | it('transforms PropTypes that are a class property', () => { 223 | const input = ` 224 | import React from 'react'; 225 | 226 | export default class Test extends React.Component { 227 | static propTypes = { 228 | optionalArray: React.PropTypes.array, 229 | optionalBool: React.PropTypes.bool, 230 | optionalFunc: React.PropTypes.func, 231 | optionalNumber: React.PropTypes.number, 232 | optionalObject: React.PropTypes.object, 233 | optionalString: React.PropTypes.string, 234 | optionalNode: React.PropTypes.node, 235 | optionalElement: React.PropTypes.element, 236 | optionalMessage: React.PropTypes.instanceOf(Message), 237 | optionalEnum: React.PropTypes.oneOf(['News', 'Photos']), 238 | optionalUnion: React.PropTypes.oneOfType([ 239 | React.PropTypes.string, 240 | React.PropTypes.number, 241 | React.PropTypes.instanceOf(Message) 242 | ]), 243 | optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number), 244 | 245 | optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number), 246 | optionalObjectWithShape: React.PropTypes.shape({ 247 | color: React.PropTypes.string, 248 | fontSize: React.PropTypes.number 249 | }), 250 | requiredFunc: React.PropTypes.func.isRequired, 251 | requiredAny: React.PropTypes.any.isRequired, 252 | }; 253 | 254 | constructor(props) { 255 | super(props); 256 | } 257 | } 258 | `; 259 | 260 | expect(transformString(input)).toMatchSnapshot(); 261 | }); 262 | 263 | it('transforms PropTypes that are defined outside of class definition', () => { 264 | const input = ` 265 | import React from 'react'; 266 | 267 | export default class Test extends React.Component { 268 | componentDidMount() { 269 | } 270 | } 271 | 272 | Test.propTypes = { 273 | optionalArray: React.PropTypes.array, 274 | optionalBool: React.PropTypes.bool, 275 | optionalFunc: React.PropTypes.func, 276 | optionalNumber: React.PropTypes.number, 277 | optionalObject: React.PropTypes.object, 278 | optionalString: React.PropTypes.string, 279 | optionalNode: React.PropTypes.node, 280 | optionalElement: React.PropTypes.element, 281 | optionalMessage: React.PropTypes.instanceOf(Message), 282 | optionalEnum: React.PropTypes.oneOf(['News', 'Photos']), 283 | optionalUnion: React.PropTypes.oneOfType([ 284 | React.PropTypes.string, 285 | React.PropTypes.number, 286 | React.PropTypes.instanceOf(Message) 287 | ]), 288 | optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number), 289 | 290 | optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number), 291 | optionalObjectWithShape: React.PropTypes.shape({ 292 | color: React.PropTypes.string, 293 | fontSize: React.PropTypes.number 294 | }), 295 | requiredFunc: React.PropTypes.func.isRequired, 296 | requiredAny: React.PropTypes.any.isRequired, 297 | }; 298 | `; 299 | 300 | expect(transformString(input)).toMatchSnapshot(); 301 | }); 302 | 303 | it('adds type annotation to `prop` parameter in constructor (ES2015)', () => { 304 | const input = ` 305 | /* @flow */ 306 | import React from 'react'; 307 | 308 | export default class Component extends React.Component { 309 | constructor(props) { 310 | super(props); 311 | } 312 | 313 | componentDidMount() { 314 | } 315 | } 316 | 317 | class Component2 extends React.Component { 318 | constructor(props) { 319 | super(props); 320 | } 321 | 322 | componentDidMount() { 323 | } 324 | } 325 | `; 326 | 327 | expect(transformString(input)).toMatchSnapshot(); 328 | }); 329 | 330 | it('preserves comments', () => { 331 | const input = ` 332 | import React from 'react'; 333 | 334 | function Button(props) { 335 | return ( 336 | 339 | ); 340 | } 341 | 342 | Button.propTypes = { 343 | // You can declare that a prop is a specific JS primitive. By default, these 344 | // are all optional. 345 | optionalArray: React.PropTypes.array, 346 | optionalBool: React.PropTypes.bool, 347 | optionalFunc: React.PropTypes.func, 348 | optionalNumber: React.PropTypes.number, 349 | optionalObject: React.PropTypes.object, 350 | optionalString: React.PropTypes.string, 351 | 352 | // Anything that can be rendered: numbers, strings, elements or an array 353 | // (or fragment) containing these types. 354 | optionalNode: React.PropTypes.node, 355 | 356 | // A React element. 357 | optionalElement: React.PropTypes.element, 358 | 359 | // You can also declare that a prop is an instance of a class. This uses 360 | // JS's instanceof operator. 361 | optionalMessage: React.PropTypes.instanceOf(Message), 362 | 363 | // You can ensure that your prop is limited to specific values by treating 364 | // it as an enum. 365 | optionalEnum: React.PropTypes.oneOf(['News', 'Photos']), 366 | 367 | // An object that could be one of many types 368 | optionalUnion: React.PropTypes.oneOfType([ 369 | React.PropTypes.string, 370 | React.PropTypes.number, 371 | React.PropTypes.instanceOf(Message) 372 | ]), 373 | 374 | // An array of a certain type 375 | optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number), 376 | 377 | // An object with property values of a certain type 378 | optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number), 379 | 380 | // An object taking on a particular shape 381 | optionalObjectWithShape: React.PropTypes.shape({ 382 | color: React.PropTypes.string, 383 | fontSize: React.PropTypes.number 384 | }), 385 | 386 | // You can chain any of the above with \`isRequired\` to make sure a warning 387 | // is shown if the prop isn't provided. 388 | requiredFunc: React.PropTypes.func.isRequired, 389 | 390 | // A value of any data type 391 | requiredAny: React.PropTypes.any.isRequired, 392 | }; 393 | `; 394 | 395 | expect(transformString(input)).toMatchSnapshot(); 396 | }); 397 | 398 | it('add empty PropTypes (no constructor)', () => { 399 | const input = ` 400 | import React from 'react'; 401 | import { View } from 'react-native'; 402 | 403 | class Cards extends React.Component { 404 | render() { 405 | return ( 406 | 407 | ); 408 | } 409 | } 410 | 411 | export default Cards; 412 | `; 413 | 414 | expect(transformString(input)).toMatchSnapshot(); 415 | }); 416 | 417 | it('adds empty PropTypes (constructor)', () => { 418 | const input = ` 419 | import { Component } from 'react'; 420 | import { View } from 'react-native'; 421 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 422 | 423 | class PureComponent extends Component { 424 | constructor(props) { 425 | super(props); 426 | 427 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 428 | } 429 | render() { 430 | return ( 431 | 432 | ); 433 | } 434 | } 435 | 436 | export default PureComponent; 437 | `; 438 | 439 | expect(transformString(input)).toMatchSnapshot(); 440 | }); 441 | 442 | it('does not touch non React classes', () => { 443 | const input = ` 444 | class PureComponent extends Class { 445 | constructor() { 446 | } 447 | } 448 | 449 | export default PureComponent; 450 | `; 451 | 452 | expect(transformString(input)).toMatchSnapshot(); 453 | }); 454 | 455 | it('transforms something that just looks like React class', () => { 456 | const input = ` 457 | import React from 'react'; 458 | import PureComponent from '../PureComponent'; 459 | 460 | class Test extends PureComponent { 461 | render() { 462 | return ( 463 |
464 | ); 465 | } 466 | } 467 | 468 | Test.propTypes = { 469 | optionalArray: React.PropTypes.array, 470 | }; 471 | 472 | export default Test; 473 | `; 474 | 475 | expect(transformString(input)).toMatchSnapshot(); 476 | }); 477 | 478 | it('handles functional components with expression body', () => { 479 | const input = ` 480 | import React, { PropTypes } from 'react'; 481 | const MyComponent = ({ hello }) =>
{hello}
; 482 | MyComponent.propTypes = { 483 | hello: PropTypes.string.isRequired, 484 | }; 485 | export default MyComponent; 486 | `; 487 | 488 | expect(transformString(input)).toMatchSnapshot(); 489 | }); 490 | 491 | it('handles block comments', () => { 492 | const input = ` 493 | import React from 'react'; 494 | 495 | export default class Test extends React.Component { 496 | static propTypes = { 497 | /** 498 | * block comment 499 | */ 500 | optionalArray: React.PropTypes.array, 501 | anotherProp: React.PropTypes.string, 502 | }; 503 | } 504 | `; 505 | 506 | expect(transformString(input)).toMatchSnapshot(); 507 | }); 508 | 509 | it('handles presence of defaultProps', () => { 510 | const input = ` 511 | import React from 'react'; 512 | 513 | export default class Test extends React.Component { 514 | static propTypes = { 515 | /** 516 | * block comment 517 | */ 518 | optionalArray: React.PropTypes.array, 519 | anotherProp: React.PropTypes.string, 520 | }; 521 | 522 | static defaultProps = { 523 | anotherProp: '' 524 | }; 525 | } 526 | `; 527 | 528 | const output = transformString(input); 529 | expect(output).toContain('type Props ='); 530 | expect(output).toMatchSnapshot(); 531 | }); 532 | 533 | it('does not touch files with flow Props already declared', () => { 534 | const input = ` 535 | /* @flow */ 536 | import React from 'react'; 537 | 538 | export type Props = { 539 | created_at?: string, 540 | }; 541 | 542 | class MyComponent extends React.Component { 543 | props: Props; 544 | 545 | render() { 546 | return ( 547 |
548 | ); 549 | } 550 | } 551 | 552 | export default MyComponent; 553 | `; 554 | 555 | expect(transformString(input)).toMatchSnapshot(); 556 | }); 557 | }); 558 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codemod-proptypes-to-flow", 3 | "version": "0.0.1", 4 | "description": "A codemod to use Flowtype instead of React.PropTypes", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "babel src --out-dir lib", 8 | "check": "npm run lint:bail && npm run test:coverage", 9 | "prepublishOnly": "npm run check && npm run build", 10 | "precommit": "lint-staged", 11 | "lint:bail": "eslint src __tests__", 12 | "lint": "eslint src __tests__ --fix", 13 | "test": "jest", 14 | "test:coverage": "jest --coverage", 15 | "test:watch": "jest --watch" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/billyvg/codemod-proptypes-to-flow.git" 20 | }, 21 | "keywords": [ 22 | "codemod", 23 | "react", 24 | "flow", 25 | "flowtype", 26 | "jscodeshift" 27 | ], 28 | "author": "Billy Vong ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/billyvg/codemod-proptypes-to-flow/issues" 32 | }, 33 | "homepage": "https://github.com/billyvg/codemod-proptypes-to-flow#readme", 34 | "devDependencies": { 35 | "babel-cli": "^6.18.0", 36 | "babel-eslint": "^6.0.2", 37 | "babel-jest": "^17.0.2", 38 | "babel-preset-es2015": "^6.18.0", 39 | "babel-preset-stage-0": "^6.16.0", 40 | "eslint": "^3.9.1", 41 | "eslint-config-airbnb-base": "^10.0.1", 42 | "eslint-config-prettier": "^2.1.1", 43 | "eslint-plugin-import": "^2.2.0", 44 | "eslint-plugin-prettier": "^2.1.2", 45 | "husky": "^0.13.3", 46 | "jest": "^19.0.2", 47 | "jscodeshift": "^0.3.30", 48 | "lint-staged": "^3.4.1", 49 | "prettier": "^1.3.1" 50 | }, 51 | "lint-staged": { 52 | "*.js": [ 53 | "eslint --fix", 54 | "npm run test --bail --findRelatedTests" 55 | ] 56 | }, 57 | "jest": { 58 | "collectCoverageFrom": [ 59 | "src/**/*.js" 60 | ], 61 | "coverageReporters": [ 62 | "json", 63 | "text", 64 | "html" 65 | ], 66 | "coveragePathIgnorePatterns": [ 67 | "/node_modules/", 68 | "/__tests__/" 69 | ], 70 | "testEnvironment": "node" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/helpers/ReactUtils.js: -------------------------------------------------------------------------------- 1 | // Origin: https://github.com/reactjs/react-codemod/blob/master/transforms/utils/ReactUtils.js 2 | /* eslint-disable func-names */ 3 | /** 4 | * Copyright 2013-2015, Facebook, Inc. 5 | * All rights reserved. 6 | * 7 | * This source code is licensed under the BSD-style license found in the 8 | * LICENSE file in the root directory of this source tree. An additional grant 9 | * of patent rights can be found in the PATENTS file in the same directory. 10 | * 11 | */ 12 | 13 | module.exports = function(j) { 14 | const REACT_CREATE_CLASS_MEMBER_EXPRESSION = { 15 | type: 'MemberExpression', 16 | object: { 17 | name: 'React', 18 | }, 19 | property: { 20 | name: 'createClass', 21 | }, 22 | }; 23 | 24 | // --------------------------------------------------------------------------- 25 | // Checks if the file requires a certain module 26 | const hasModule = (path, module) => 27 | path 28 | .findVariableDeclarators() 29 | .filter(j.filters.VariableDeclarator.requiresModule(module)) 30 | .size() === 1 || 31 | path 32 | .find(j.ImportDeclaration, { 33 | type: 'ImportDeclaration', 34 | source: { 35 | type: 'Literal', 36 | }, 37 | }) 38 | .filter(declarator => declarator.value.source.value === module) 39 | .size() === 1; 40 | 41 | const hasReact = path => 42 | hasModule(path, 'React') || 43 | hasModule(path, 'react') || 44 | hasModule(path, 'react/addons') || 45 | hasModule(path, 'react-native'); 46 | 47 | // --------------------------------------------------------------------------- 48 | // Finds all variable declarations that call React.createClass 49 | const findReactCreateClassCallExpression = path => 50 | j(path).find(j.CallExpression, { 51 | callee: REACT_CREATE_CLASS_MEMBER_EXPRESSION, 52 | }); 53 | 54 | const findReactCreateClass = path => 55 | path 56 | .findVariableDeclarators() 57 | .filter(decl => findReactCreateClassCallExpression(decl).size() > 0); 58 | 59 | const findReactCreateClassExportDefault = path => 60 | path.find(j.ExportDeclaration, { 61 | default: true, 62 | declaration: { 63 | type: 'CallExpression', 64 | callee: REACT_CREATE_CLASS_MEMBER_EXPRESSION, 65 | }, 66 | }); 67 | 68 | const findReactCreateClassModuleExports = path => 69 | path.find(j.AssignmentExpression, { 70 | left: { 71 | type: 'MemberExpression', 72 | object: { 73 | type: 'Identifier', 74 | name: 'module', 75 | }, 76 | property: { 77 | type: 'Identifier', 78 | name: 'exports', 79 | }, 80 | }, 81 | right: { 82 | type: 'CallExpression', 83 | callee: REACT_CREATE_CLASS_MEMBER_EXPRESSION, 84 | }, 85 | }); 86 | 87 | const getReactCreateClassSpec = classPath => { 88 | const { value } = classPath; 89 | const args = (value.init || value.right || value.declaration).arguments; 90 | if (args && args.length) { 91 | const spec = args[0]; 92 | if (spec.type === 'ObjectExpression' && Array.isArray(spec.properties)) { 93 | return spec; 94 | } 95 | } 96 | return null; 97 | }; 98 | 99 | // --------------------------------------------------------------------------- 100 | // Finds alias for React.Component if used as named import. 101 | const findReactComponentName = path => { 102 | const reactImportDeclaration = path 103 | .find(j.ImportDeclaration, { 104 | type: 'ImportDeclaration', 105 | source: { 106 | type: 'Literal', 107 | }, 108 | }) 109 | .filter(importDeclaration => hasReact(path)); 110 | 111 | const componentImportSpecifier = reactImportDeclaration 112 | .find(j.ImportSpecifier, { 113 | type: 'ImportSpecifier', 114 | imported: { 115 | type: 'Identifier', 116 | name: 'Component', 117 | }, 118 | }) 119 | .at(0); 120 | 121 | const paths = componentImportSpecifier.paths(); 122 | return paths.length ? paths[0].value.local.name : undefined; 123 | }; 124 | 125 | // Finds all classes that extend React.Component 126 | const findReactES6ClassDeclaration = path => { 127 | const componentImport = findReactComponentName(path); 128 | const selector = componentImport 129 | ? { 130 | superClass: { 131 | type: 'Identifier', 132 | name: componentImport, 133 | }, 134 | } 135 | : { 136 | superClass: { 137 | type: 'MemberExpression', 138 | object: { 139 | type: 'Identifier', 140 | name: 'React', 141 | }, 142 | property: { 143 | type: 'Identifier', 144 | name: 'Component', 145 | }, 146 | }, 147 | }; 148 | 149 | return path.find(j.ClassDeclaration, selector); 150 | }; 151 | 152 | // --------------------------------------------------------------------------- 153 | // Checks if the React class has mixins 154 | const isMixinProperty = property => { 155 | const key = property.key; 156 | const value = property.value; 157 | return ( 158 | key.name === 'mixins' && 159 | value.type === 'ArrayExpression' && 160 | Array.isArray(value.elements) && 161 | value.elements.length 162 | ); 163 | }; 164 | 165 | const hasMixins = classPath => { 166 | const spec = getReactCreateClassSpec(classPath); 167 | return spec && spec.properties.some(isMixinProperty); 168 | }; 169 | 170 | // --------------------------------------------------------------------------- 171 | // Others 172 | const getClassExtendReactSpec = classPath => classPath.value.body; 173 | 174 | const createCreateReactClassCallExpression = properties => 175 | j.callExpression( 176 | j.memberExpression( 177 | j.identifier('React'), 178 | j.identifier('createClass'), 179 | false 180 | ), 181 | [j.objectExpression(properties)] 182 | ); 183 | 184 | const getComponentName = classPath => 185 | classPath.node.id && classPath.node.id.name; 186 | 187 | // --------------------------------------------------------------------------- 188 | // Direct methods! (see explanation below) 189 | const findAllReactCreateClassCalls = path => 190 | path.find(j.CallExpression, { 191 | callee: REACT_CREATE_CLASS_MEMBER_EXPRESSION, 192 | }); 193 | 194 | // Mixin Stuff 195 | const containSameElements = (ls1, ls2) => { 196 | if (ls1.length !== ls2.length) { 197 | return false; 198 | } 199 | 200 | return ( 201 | ls1.reduce((res, x) => res && ls2.indexOf(x) !== -1, true) && 202 | ls2.reduce((res, x) => res && ls1.indexOf(x) !== -1, true) 203 | ); 204 | }; 205 | 206 | const keyNameIsMixins = property => property.key.name === 'mixins'; 207 | 208 | const isSpecificMixinsProperty = (property, mixinIdentifierNames) => { 209 | const key = property.key; 210 | const value = property.value; 211 | 212 | return ( 213 | key.name === 'mixins' && 214 | value.type === 'ArrayExpression' && 215 | Array.isArray(value.elements) && 216 | value.elements.every(elem => elem.type === 'Identifier') && 217 | containSameElements( 218 | value.elements.map(elem => elem.name), 219 | mixinIdentifierNames 220 | ) 221 | ); 222 | }; 223 | 224 | // These following methods assume that the argument is 225 | // a `React.createClass` call expression. In other words, 226 | // they should only be used with `findAllReactCreateClassCalls`. 227 | const directlyGetCreateClassSpec = classPath => { 228 | if (!classPath || !classPath.value) { 229 | return null; 230 | } 231 | const args = classPath.value.arguments; 232 | if (args && args.length) { 233 | const spec = args[0]; 234 | if (spec.type === 'ObjectExpression' && Array.isArray(spec.properties)) { 235 | return spec; 236 | } 237 | } 238 | return null; 239 | }; 240 | 241 | const directlyGetComponentName = classPath => { 242 | let result = ''; 243 | if ( 244 | classPath.parentPath.value && 245 | classPath.parentPath.value.type === 'VariableDeclarator' 246 | ) { 247 | result = classPath.parentPath.value.id.name; 248 | } 249 | return result; 250 | }; 251 | 252 | const directlyHasMixinsField = classPath => { 253 | const spec = directlyGetCreateClassSpec(classPath); 254 | return spec && spec.properties.some(keyNameIsMixins); 255 | }; 256 | 257 | const directlyHasSpecificMixins = (classPath, mixinIdentifierNames) => { 258 | const spec = directlyGetCreateClassSpec(classPath); 259 | return ( 260 | spec && 261 | spec.properties.some(prop => 262 | isSpecificMixinsProperty(prop, mixinIdentifierNames) 263 | ) 264 | ); 265 | }; 266 | 267 | return { 268 | createCreateReactClassCallExpression, 269 | findReactES6ClassDeclaration, 270 | findReactCreateClass, 271 | findReactCreateClassCallExpression, 272 | findReactCreateClassModuleExports, 273 | findReactCreateClassExportDefault, 274 | getComponentName, 275 | getReactCreateClassSpec, 276 | getClassExtendReactSpec, 277 | hasMixins, 278 | hasModule, 279 | hasReact, 280 | isMixinProperty, 281 | 282 | // "direct" methods 283 | findAllReactCreateClassCalls, 284 | directlyGetComponentName, 285 | directlyGetCreateClassSpec, 286 | directlyHasMixinsField, 287 | directlyHasSpecificMixins, 288 | }; 289 | }; 290 | -------------------------------------------------------------------------------- /src/helpers/annotateConstructor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Annotates ES2015 Class constructor and Class `props` member 3 | * 4 | * @param {jscodeshiftApi} j jscodeshift API 5 | * @param {Array} body Array of `Node` 6 | */ 7 | export default function annotateConstructor(j, body, name = 'Props') { 8 | let constructorIndex; 9 | const typeAnnotation = j.typeAnnotation( 10 | j.genericTypeAnnotation(j.identifier(name), null) 11 | ); 12 | 13 | body.some((b, i) => { 14 | if (b.kind === 'constructor') { 15 | constructorIndex = i + 1; 16 | 17 | // first parameter is always props regardless of name 18 | if (b.value.params && b.value.params.length) { 19 | b.value.params[0].typeAnnotation = typeAnnotation; 20 | } 21 | return true; 22 | } 23 | }); 24 | 25 | body.splice( 26 | constructorIndex, 27 | 0, 28 | j.classProperty(j.identifier('props'), null, typeAnnotation) 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/helpers/createTypeAlias.js: -------------------------------------------------------------------------------- 1 | export default function createTypeAlias( 2 | j, 3 | flowTypes, 4 | { name = 'Props', shouldExport = false } = {} 5 | ) { 6 | const typeAlias = j.typeAlias( 7 | j.identifier(name), 8 | null, 9 | j.objectTypeAnnotation(flowTypes) 10 | ); 11 | 12 | if (shouldExport) { 13 | return j.exportNamedDeclaration(typeAlias); 14 | } 15 | 16 | return typeAlias; 17 | } 18 | -------------------------------------------------------------------------------- /src/helpers/findIndex.js: -------------------------------------------------------------------------------- 1 | export default function findIndex(arr, f) { 2 | let index; 3 | arr.some((val, i) => { 4 | const result = f(val, i); 5 | if (result) { 6 | index = i; 7 | } 8 | return result; 9 | }); 10 | 11 | return index; 12 | } 13 | -------------------------------------------------------------------------------- /src/helpers/findParentBody.js: -------------------------------------------------------------------------------- 1 | export default function findParentBody(p, memo) { 2 | if (p.parentPath) { 3 | if (p.parentPath.name === 'body') { 4 | return { 5 | child: p.value, 6 | body: p.parentPath, 7 | }; 8 | } 9 | return findParentBody(p.parentPath); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/helpers/propTypeToFlowType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Handles transforming a React.PropType to an equivalent flowtype 3 | */ 4 | export default function propTypeToFlowType(j, key, value) { 5 | /** 6 | * Returns an expression without `isRequired` 7 | * @param {Node} node NodePath Should be the `value` of a `Property` 8 | * @return {Object} Object with `required`, and `node` 9 | */ 10 | const getExpressionWithoutRequired = inputNode => { 11 | // check if it's required 12 | let required = false; 13 | let node = inputNode; 14 | 15 | if (inputNode.property && inputNode.property.name === 'isRequired') { 16 | required = true; 17 | node = inputNode.object; 18 | } 19 | 20 | return { 21 | required, 22 | node, 23 | }; 24 | }; 25 | 26 | /** 27 | * Gets the PropType MemberExpression without `React` namespace 28 | */ 29 | const getPropTypeExpression = inputNode => { 30 | if ( 31 | inputNode.object && 32 | inputNode.object.object && 33 | inputNode.object.object.name === 'React' 34 | ) { 35 | return j.memberExpression(inputNode.object.property, inputNode.property); 36 | } else if (inputNode.object && inputNode.object.name === 'React') { 37 | return inputNode.property; 38 | } 39 | return inputNode; 40 | }; 41 | 42 | const TRANSFORM_MAP = { 43 | any: j.anyTypeAnnotation(), 44 | bool: j.booleanTypeAnnotation(), 45 | func: j.genericTypeAnnotation(j.identifier('Function'), null), 46 | number: j.numberTypeAnnotation(), 47 | object: j.genericTypeAnnotation(j.identifier('Object'), null), 48 | string: j.stringTypeAnnotation(), 49 | str: j.stringTypeAnnotation(), 50 | array: j.genericTypeAnnotation( 51 | j.identifier('Array'), 52 | j.typeParameterInstantiation([j.anyTypeAnnotation()]) 53 | ), 54 | element: j.genericTypeAnnotation( 55 | j.qualifiedTypeIdentifier(j.identifier('React'), j.identifier('Element')), 56 | null 57 | ), 58 | node: j.unionTypeAnnotation([ 59 | j.numberTypeAnnotation(), 60 | j.stringTypeAnnotation(), 61 | j.genericTypeAnnotation( 62 | j.qualifiedTypeIdentifier( 63 | j.identifier('React'), 64 | j.identifier('Element') 65 | ), 66 | null 67 | ), 68 | j.genericTypeAnnotation( 69 | j.identifier('Array'), 70 | j.typeParameterInstantiation([j.anyTypeAnnotation()]) 71 | ), 72 | ]), 73 | }; 74 | let returnValue; 75 | 76 | const expressionWithoutRequired = getExpressionWithoutRequired(value); 77 | const required = expressionWithoutRequired.required; 78 | const node = expressionWithoutRequired.node; 79 | 80 | // Check for React namespace for MemberExpressions (i.e. React.PropTypes.string) 81 | if (node.object) { 82 | node.object = getPropTypeExpression(node.object); 83 | } else if (node.callee) { 84 | node.callee = getPropTypeExpression(node.callee); 85 | } 86 | 87 | if (node.type === 'Literal') { 88 | returnValue = j.stringLiteralTypeAnnotation(node.value, node.raw); 89 | } else if (node.type === 'MemberExpression') { 90 | returnValue = TRANSFORM_MAP[node.property.name]; 91 | } else if (node.type === 'CallExpression') { 92 | // instanceOf(), arrayOf(), etc.. 93 | const name = node.callee.property.name; 94 | if (name === 'instanceOf') { 95 | returnValue = j.genericTypeAnnotation(node.arguments[0], null); 96 | } else if (name === 'arrayOf') { 97 | returnValue = j.genericTypeAnnotation( 98 | j.identifier('Array'), 99 | j.typeParameterInstantiation([ 100 | propTypeToFlowType( 101 | j, 102 | null, 103 | node.arguments[0] || j.anyTypeAnnotation() 104 | ), 105 | ]) 106 | ); 107 | } else if (name === 'objectOf') { 108 | // TODO: Is there a direct Flow translation for this? 109 | returnValue = j.genericTypeAnnotation( 110 | j.identifier('Object'), 111 | j.typeParameterInstantiation([ 112 | propTypeToFlowType( 113 | j, 114 | null, 115 | node.arguments[0] || j.anyTypeAnnotation() 116 | ), 117 | ]) 118 | ); 119 | } else if (name === 'shape') { 120 | returnValue = j.objectTypeAnnotation( 121 | node.arguments[0].properties.map(arg => 122 | propTypeToFlowType(j, arg.key, arg.value) 123 | ) 124 | ); 125 | } else if (name === 'oneOfType' || name === 'oneOf') { 126 | returnValue = j.unionTypeAnnotation( 127 | node.arguments[0].elements.map(arg => propTypeToFlowType(j, null, arg)) 128 | ); 129 | } 130 | } else if (node.type === 'ObjectExpression') { 131 | returnValue = j.objectTypeAnnotation( 132 | node.arguments.map(arg => propTypeToFlowType(j, arg.key, arg.value)) 133 | ); 134 | } else if (node.type === 'Identifier') { 135 | returnValue = j.genericTypeAnnotation(node, null); 136 | } 137 | 138 | // finally return either an objectTypeProperty or just a property if `key` is null 139 | if (!key) { 140 | return returnValue; 141 | } else { 142 | return j.objectTypeProperty(key, returnValue, !required); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/helpers/removePropTypeImport.js: -------------------------------------------------------------------------------- 1 | export default function removePropTypeImport(j, ast) { 2 | // remove `PropTypes` from import React, { PropTypes } from 'react' 3 | ast 4 | .find(j.ImportDeclaration, { 5 | type: 'ImportDeclaration', 6 | source: { 7 | type: 'Literal', 8 | value: 'react', 9 | }, 10 | }) 11 | .find(j.ImportSpecifier, { imported: { name: 'PropTypes' } }) 12 | .remove(); 13 | 14 | // remove whole line import { PropTypes } from 'react' 15 | ast 16 | .find(j.ImportDeclaration, { 17 | type: 'ImportDeclaration', 18 | source: { 19 | type: 'Literal', 20 | value: 'react', 21 | }, 22 | }) 23 | .filter(p => p.value.specifiers.length === 0) 24 | .remove(); 25 | 26 | // remove react16 import PropType from 'prop-types' or import { bool } from 'prop-types' 27 | ast 28 | .find(j.ImportDeclaration, { 29 | type: 'ImportDeclaration', 30 | source: { 31 | type: 'Literal', 32 | value: 'prop-types', 33 | }, 34 | }) 35 | .remove(); 36 | } 37 | -------------------------------------------------------------------------------- /src/helpers/transformProperties.js: -------------------------------------------------------------------------------- 1 | import propTypeToFlowType from './propTypeToFlowType'; 2 | 3 | export default function transformProperties(j, properties) { 4 | return properties.map(property => { 5 | const type = propTypeToFlowType(j, property.key, property.value); 6 | type.leadingComments = property.leadingComments; 7 | type.comments = property.comments; 8 | return type; 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import transformEs6ClassComponents from './transformers/es6Classes'; 2 | import transformFunctionalComponents from './transformers/functional'; 3 | import ReactUtils from './helpers/ReactUtils'; 4 | 5 | function addFlowComment(j, ast, options) { 6 | const getBodyNode = () => ast.find(j.Program).get('body', 0).node; 7 | 8 | const comments = getBodyNode().comments || []; 9 | const containsFlowComment = 10 | comments.filter(e => e.value.indexOf('@flow') !== -1).length > 0; 11 | 12 | if (!containsFlowComment) { 13 | switch (options.flowComment) { 14 | case 'line': 15 | comments.unshift(j.commentLine(' @flow')); 16 | break; 17 | case 'block': 18 | default: 19 | comments.unshift(j.commentBlock(' @flow ')); 20 | break; 21 | } 22 | } 23 | 24 | getBodyNode().comments = comments; 25 | } 26 | 27 | export default function transformer(file, api, rawOptions) { 28 | const j = api.jscodeshift; 29 | const root = j(file.source); 30 | 31 | const options = rawOptions; 32 | if (options.flowComment !== 'line' && options.flowComment !== 'block') { 33 | if (options.flowComment) { 34 | console.warn( 35 | `Unsupported flowComment value provided: ${options.flowComment}` 36 | ); 37 | console.warn('Supported options are "block" and "line".'); 38 | console.warn('Falling back to default: "block".'); 39 | } 40 | options.flowComment = 'block'; 41 | } 42 | if (!options.propsTypeSuffix) { 43 | options.propsTypeSuffix = 'Props'; 44 | } 45 | 46 | const reactUtils = ReactUtils(j); 47 | if (!reactUtils.hasReact(root)) { 48 | return file.source; 49 | } 50 | 51 | const classModifications = transformEs6ClassComponents(root, j, options); 52 | const functionalModifications = transformFunctionalComponents( 53 | root, 54 | j, 55 | options 56 | ); 57 | 58 | if (classModifications || functionalModifications) { 59 | addFlowComment(j, root, options); 60 | return root.toSource({ quote: 'single', trailingComma: true }); 61 | } else { 62 | return file.source; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/transformers/es6Classes.js: -------------------------------------------------------------------------------- 1 | import annotateConstructor from '../helpers/annotateConstructor'; 2 | import createTypeAlias from '../helpers/createTypeAlias'; 3 | import findIndex from '../helpers/findIndex'; 4 | import findParentBody from '../helpers/findParentBody'; 5 | import transformProperties from '../helpers/transformProperties'; 6 | import ReactUtils from '../helpers/ReactUtils'; 7 | import removePropTypeImportDeclaration from '../helpers/removePropTypeImport'; 8 | 9 | const isStaticPropType = p => { 10 | return ( 11 | p.type === 'ClassProperty' && 12 | p.static && 13 | p.key.type === 'Identifier' && 14 | p.key.name === 'propTypes' 15 | ); 16 | }; 17 | 18 | function containsFlowProps(classBody) { 19 | return !!classBody.find(bodyElement => bodyElement.key.name === 'props'); 20 | } 21 | 22 | /** 23 | * Transforms es2016 components 24 | * @return true if any components were transformed. 25 | */ 26 | export default function transformEs6Classes(ast, j, options) { 27 | const reactUtils = ReactUtils(j); 28 | 29 | const classNamesWithPropsOutside = []; 30 | 31 | // NOTE: reactUtils.findReactES6ClassDeclaration(ast) is missing extends 32 | // for local imported components... If finding all classes is too greety, 33 | // we might combine findReactES6ClassDeclaration with classes that have a 34 | // render method. 35 | const reactClassPaths = ast.find(j.ClassDeclaration); 36 | 37 | // find classes with propType static class property 38 | const modifications = reactClassPaths 39 | .forEach(p => { 40 | const className = reactUtils.getComponentName(p); 41 | const propIdentifier = reactClassPaths.length === 1 42 | ? options.propsTypeSuffix 43 | : `${className}${options.propsTypeSuffix}`; 44 | let properties; 45 | 46 | const classBody = p.value.body && p.value.body.body; 47 | if (classBody) { 48 | if (containsFlowProps(classBody)) { 49 | return; 50 | } 51 | 52 | annotateConstructor(j, classBody, propIdentifier); 53 | const index = findIndex(classBody, isStaticPropType); 54 | if (typeof index !== 'undefined') { 55 | const classProperty = classBody.splice(index, 1).pop(); 56 | properties = classProperty.value.properties; 57 | } else { 58 | // look for propTypes defined elsewhere 59 | classNamesWithPropsOutside.push(className); 60 | 61 | ast 62 | .find(j.AssignmentExpression, { 63 | left: { 64 | type: 'MemberExpression', 65 | object: { 66 | name: className, 67 | }, 68 | property: { 69 | name: 'propTypes', 70 | }, 71 | }, 72 | right: { 73 | type: 'ObjectExpression', 74 | }, 75 | }) 76 | .forEach(p => { 77 | // this should only be one? 78 | properties = p.value.right.properties; 79 | }) 80 | .remove(); 81 | } 82 | 83 | properties = properties || []; 84 | const typeAlias = createTypeAlias( 85 | j, 86 | transformProperties(j, properties), 87 | { 88 | name: propIdentifier, 89 | shouldExport: false, 90 | } 91 | ); 92 | 93 | // Find location to put propTypes flowtype definition 94 | // This will place ahead of class def 95 | const { child, body } = findParentBody(p); 96 | if (body && child) { 97 | const bodyIndex = findIndex(body.value, b => b === child); 98 | if (bodyIndex) { 99 | body.value.splice(bodyIndex, 0, typeAlias); 100 | } 101 | } 102 | } 103 | }) 104 | .size(); 105 | 106 | ast 107 | .find(j.ExpressionStatement, { 108 | expression: { 109 | type: 'AssignmentExpression', 110 | left: { 111 | type: 'MemberExpression', 112 | property: { 113 | name: 'propTypes', 114 | }, 115 | }, 116 | right: { 117 | type: 'ObjectExpression', 118 | }, 119 | }, 120 | }) 121 | .filter( 122 | p => 123 | classNamesWithPropsOutside.indexOf( 124 | p.value.expression.left.object.name 125 | ) > -1 126 | ) 127 | .remove(); 128 | 129 | removePropTypeImportDeclaration(j, ast); 130 | 131 | return modifications > 0; 132 | } 133 | -------------------------------------------------------------------------------- /src/transformers/functional.js: -------------------------------------------------------------------------------- 1 | import propTypeToFlowType from '../helpers/propTypeToFlowType'; 2 | 3 | function removeComponentAssignmentPropTypes(ast, j) { 4 | const componentToPropTypesRemoved = {}; 5 | 6 | ast 7 | .find(j.AssignmentExpression, { 8 | left: { 9 | property: { 10 | name: 'propTypes', 11 | }, 12 | }, 13 | }) 14 | .forEach(p => { 15 | const objectName = p.value.left.object.name; 16 | const properties = p.value.right.properties; 17 | const flowTypesRemoved = properties.map(property => { 18 | const t = propTypeToFlowType(j, property.key, property.value); 19 | t.comments = property.comments; 20 | return t; 21 | }); 22 | 23 | componentToPropTypesRemoved[objectName] = flowTypesRemoved; 24 | }) 25 | .remove(); 26 | 27 | return componentToPropTypesRemoved; 28 | } 29 | 30 | function insertTypeIdentifierInFunction(functionPath, j, typeIdentifier) { 31 | const functionRoot = functionPath.value.init || functionPath.value; 32 | 33 | const params = functionRoot.params; 34 | const param = params[0]; 35 | 36 | const newTypeAnnotation = j.typeAnnotation( 37 | j.genericTypeAnnotation(j.identifier(typeIdentifier), null) 38 | ); 39 | 40 | if (param.type === 'Identifier') { 41 | param.typeAnnotation = newTypeAnnotation; 42 | } else if (param.type === 'ObjectPattern') { 43 | // NOTE: something is wrong with recast and objectPatterns... 44 | // You cannot set typeAnnotation on them, do object spread instead 45 | 46 | const newProps = j.identifier('props'); 47 | newProps.typeAnnotation = newTypeAnnotation; 48 | functionRoot.params = [newProps]; 49 | const newSpread = j.variableDeclaration('const', [ 50 | j.variableDeclarator(param, j.identifier('props')), 51 | ]); 52 | 53 | // if the body of the function is an expression, we need to construct 54 | // a block statement to hold the props spread 55 | if (functionRoot.body.type === 'BlockStatement') { 56 | functionRoot.body.body.unshift(newSpread); 57 | } else { 58 | const returnExpression = j.returnStatement(functionRoot.body); 59 | functionRoot.body = j.blockStatement([newSpread, returnExpression]); 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * Transforms function components 66 | * @return true if any functional components were transformed. 67 | */ 68 | export default function transformFunctionalComponents(ast, j, options) { 69 | // Look for Foo.propTypes 70 | const componentToPropTypesRemoved = removeComponentAssignmentPropTypes( 71 | ast, 72 | j 73 | ); 74 | const components = Object.keys(componentToPropTypesRemoved); 75 | 76 | if (components.length === 0) { 77 | return null; 78 | } 79 | 80 | components.forEach(c => { 81 | const flowTypesRemoved = componentToPropTypesRemoved[c]; 82 | const propIdentifier = components.length === 1 83 | ? options.propsTypeSuffix 84 | : `${c}${options.propsTypeSuffix}`; 85 | const flowTypeProps = j.exportNamedDeclaration( 86 | j.typeAlias( 87 | j.identifier(propIdentifier), 88 | null, 89 | j.objectTypeAnnotation(flowTypesRemoved) 90 | ) 91 | ); 92 | 93 | ast 94 | .find(j.FunctionDeclaration, { 95 | id: { name: c }, 96 | }) 97 | .forEach(f => { 98 | const insertNode = f.parent.node.type === 'Program' ? f : f.parent; 99 | insertNode.insertBefore(flowTypeProps); 100 | insertTypeIdentifierInFunction(f, j, propIdentifier); 101 | }); 102 | 103 | ast 104 | .find(j.VariableDeclarator, { 105 | id: { name: c }, 106 | }) 107 | .forEach(f => { 108 | const insertNode = f.parent.parent.node.type === 'Program' 109 | ? f.parent 110 | : f.parent.parent; 111 | insertNode.insertBefore(flowTypeProps); 112 | insertTypeIdentifierInFunction(f, j, propIdentifier); 113 | }); 114 | }); 115 | 116 | return components.length > 0; 117 | } 118 | --------------------------------------------------------------------------------