Repository: billyvg/codemod-proptypes-to-flow Branch: master Commit: 632e3dcfc8f8 Files: 21 Total size: 58.8 KB Directory structure: gitextract_nfwm18v1/ ├── .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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": ["es2015", "stage-0"] } ================================================ FILE: .eslintignore ================================================ node_modules lib ================================================ FILE: .eslintrc.js ================================================ module.exports = { extends: [ 'airbnb-base', 'prettier', ], env: { 'node': true, }, root: true, plugins: [ 'prettier', ], rules: { 'comma-dangle': ['error', 'always-multiline'], 'import/order': ['error', { 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], }], 'indent': ['off', 2, {'SwitchCase': 1}], 'max-len': ['error', 120, 4, {'ignoreComments': true, 'ignoreUrls': true}], 'no-unused-vars': ['error', {'vars': 'all', 'args': 'none'}], 'space-before-function-paren': ['error', 'never'], 'space-in-parens': ['error', 'never'], 'no-underscore-dangle': 'off', 'no-param-reassign': 'off', 'no-console': 'off', 'no-warning-comments': ['warn', { 'terms': ['fixme'], 'location': 'start' }], 'prettier/prettier': ['error', {'trailingComma': 'es5', 'singleQuote': true} ], // NOTE: Disabled to not do too many changes to original codebase: 'consistent-return': 'off', 'arrow-body-style': 'off', 'object-curly-spacing': 'off', 'padded-blocks': 'off', 'arrow-parens': 'off', 'import/extensions': 'off', 'no-shadow': 'off', 'array-callback-return': 'off', 'no-else-return': 'off', } } ================================================ FILE: .gitignore ================================================ node_modules npm-debug.log* lib .DS_Store coverage ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - '7' - '6' - '4' before_install: - npm install -g codecov script: - yarn run check - yarn run build - codecov cache: yarn after_success: - bash <(curl -s https://codecov.io/bash) -f coverage/coverage-final.json ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Billy Vong Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # 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) Removes `React.PropTypes` and attempts to transform to [Flow](http://flow.org/). ### Setup & Run * `npm install -g jscodeshift` * `git clone https://github.com/billyvg/codemod-proptypes-to-flow` * `jscodeshift -t codemod-proptypes-to-flow/src/index.js ` * Use the `-d` option for a dry-run and use `-p` to print the output for comparison #### Options Behavior of this codemod can be customized by passing options to jscodeshift e.g.: ``` jscodeshift -t codemod-proptypes-to-flow/src/index.js --flowComment=line ``` Following options are accepted: ##### flowComment `--flowComment=` - type of flow comment. Defaults to `block`. ``` --flowComment=block: /* @flow */ --flowComment=line: // @flow ``` ##### propsTypeSuffix `--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`. Default: ``` type Props = {...} type MyComponentProps = {...} ``` With `--propsTypeSuffix=PropsType`: ``` type PropsType = {...} type MyComponentPropsType = {...} ``` ### Not working/Implemented yet * Custom validators * `React.createClass` * Use of importing PropTypes ### Contributors * Thanks to [@skovhus](https://github.com/skovhus) for adding support for functional components and modernizing the codebase a bit (a lot) ================================================ FILE: __tests__/__snapshots__/proptypes-to-flow-test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`React.PropTypes to flow add empty PropTypes (no constructor) 1`] = ` " /* @flow */ import React from 'react'; import { View } from 'react-native'; type Props = {}; class Cards extends React.Component { props: Props; render() { return ( ); } } export default Cards; " `; exports[`React.PropTypes to flow adds empty PropTypes (constructor) 1`] = ` " /* @flow */ import { Component } from 'react'; import { View } from 'react-native'; import PureRenderMixin from 'react-addons-pure-render-mixin'; type Props = {}; class PureComponent extends Component { constructor(props: Props) { super(props); this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); } props: Props; render() { return ( ); } } export default PureComponent; " `; exports[`React.PropTypes to flow adds type annotation to \`prop\` parameter in constructor (ES2015) 1`] = ` " /* @flow */ import React from 'react'; type ComponentProps = {}; export default class Component extends React.Component { constructor(props: ComponentProps) { super(props); } props: ComponentProps; componentDidMount() { } } type Component2Props = {}; class Component2 extends React.Component { constructor(props: Component2Props) { super(props); } props: Component2Props; componentDidMount() { } } " `; exports[`React.PropTypes to flow does not touch files with flow Props already declared 1`] = ` " /* @flow */ import React from 'react'; export type Props = { created_at?: string, }; class MyComponent extends React.Component { props: Props; render() { return (
); } } export default MyComponent; " `; exports[`React.PropTypes to flow does not touch non React classes 1`] = ` " class PureComponent extends Class { constructor() { } } export default PureComponent; " `; exports[`React.PropTypes to flow handles block comments 1`] = ` " /* @flow */ import React from 'react'; type Props = { /** * block comment */ optionalArray?: Array, anotherProp?: string, }; export default class Test extends React.Component { props: Props; } " `; exports[`React.PropTypes to flow handles functional components with expression body 1`] = ` " /* @flow */ import React from 'react'; export type Props = { hello: string }; const MyComponent = (props: Props) => { const { hello } = props; return
{hello}
; }; export default MyComponent; " `; exports[`React.PropTypes to flow handles presence of defaultProps 1`] = ` " /* @flow */ import React from 'react'; type Props = { /** * block comment */ optionalArray?: Array, anotherProp?: string, }; export default class Test extends React.Component { props: Props; static defaultProps = { anotherProp: '' }; } " `; exports[`React.PropTypes to flow preserves comments 1`] = ` " /* @flow */ import React from 'react'; export type Props = { // You can declare that a prop is a specific JS primitive. By default, these // are all optional. optionalArray?: Array, optionalBool?: boolean, optionalFunc?: Function, optionalNumber?: number, optionalObject?: Object, optionalString?: string, // Anything that can be rendered: numbers, strings, elements or an array // (or fragment) containing these types. optionalNode?: number | string | React.Element | Array, // A React element. optionalElement?: React.Element, // You can also declare that a prop is an instance of a class. This uses // JS's instanceof operator. optionalMessage?: Message, // You can ensure that your prop is limited to specific values by treating // it as an enum. optionalEnum?: 'News' | 'Photos', // An object that could be one of many types optionalUnion?: string | number | Message, // An array of a certain type optionalArrayOf?: Array, // An object with property values of a certain type optionalObjectOf?: Object, // An object taking on a particular shape optionalObjectWithShape?: { color?: string, fontSize?: number, }, // You can chain any of the above with \`isRequired\` to make sure a warning // is shown if the prop isn't provided. requiredFunc: Function, // A value of any data type requiredAny: any, }; function Button(props: Props) { return ( ); } " `; exports[`React.PropTypes to flow removes react's 16 PropTypes import 1`] = ` " /* @flow */ import React from 'react'; export type Props = { optionalArray?: Array, optionalBool?: boolean, }; function Button(props: Props) { return ( ); } " `; exports[`React.PropTypes to flow removes react's 16 destructured PropTypes import 1`] = ` " /* @flow */ import React from 'react'; export type Props = { optionalArray?: Array }; function Button(props: Props) { return ( ); } " `; exports[`React.PropTypes to flow transforms PropTypes that are a class property 1`] = ` " /* @flow */ import React from 'react'; type Props = { optionalArray?: Array, optionalBool?: boolean, optionalFunc?: Function, optionalNumber?: number, optionalObject?: Object, optionalString?: string, optionalNode?: number | string | React.Element | Array, optionalElement?: React.Element, optionalMessage?: Message, optionalEnum?: 'News' | 'Photos', optionalUnion?: string | number | Message, optionalArrayOf?: Array, optionalObjectOf?: Object, optionalObjectWithShape?: { color?: string, fontSize?: number, }, requiredFunc: Function, requiredAny: any, }; export default class Test extends React.Component { constructor(props: Props) { super(props); } props: Props; } " `; exports[`React.PropTypes to flow transforms PropTypes that are defined outside of class definition 1`] = ` " /* @flow */ import React from 'react'; type Props = { optionalArray?: Array, optionalBool?: boolean, optionalFunc?: Function, optionalNumber?: number, optionalObject?: Object, optionalString?: string, optionalNode?: number | string | React.Element | Array, optionalElement?: React.Element, optionalMessage?: Message, optionalEnum?: 'News' | 'Photos', optionalUnion?: string | number | Message, optionalArrayOf?: Array, optionalObjectOf?: Object, optionalObjectWithShape?: { color?: string, fontSize?: number, }, requiredFunc: Function, requiredAny: any, }; export default class Test extends React.Component { props: Props; componentDidMount() { } } " `; exports[`React.PropTypes to flow transforms optional PropTypes prefixed with \`React\` 1`] = ` " /* @flow */ import React from 'react'; export type Props = { optionalArray?: Array, optionalBool?: boolean, optionalFunc?: Function, optionalNumber?: number, optionalObject?: Object, optionalString?: string, optionalNode?: number | string | React.Element | Array, optionalElement?: React.Element, optionalMessage?: Message, optionalEnum?: 'News' | 'Photos', optionalUnion?: string | number | Message, optionalArrayOf?: Array, optionalObjectOf?: Object, optionalObjectWithShape?: { color?: string, fontSize?: number, }, }; export const F = (props: Props) =>
; " `; exports[`React.PropTypes to flow transforms optional PropTypes with no \`React\` prefix 1`] = ` " /* @flow */ import React from 'react'; export type Props = { optionalArray?: Array, optionalBool?: boolean, optionalFunc?: Function, optionalNumber?: number, optionalObject?: Object, optionalString?: string, optionalNode?: number | string | React.Element | Array, optionalElement?: React.Element, optionalMessage?: Message, optionalEnum?: 'News' | 'Photos', optionalUnion?: string | number | Message, optionalArrayOf?: Array, optionalObjectOf?: Object, optionalObjectWithShape?: { color?: string, fontSize?: number, }, }; function Button(props: Props) { return ( ); } " `; exports[`React.PropTypes to flow transforms required PropTypes prefixed with \`React\` 1`] = ` " /* @flow */ /* eslint */ import React from 'react'; export type ButtonProps = { requiredArray: Array, requiredBool: boolean, requiredFunc: Function, requiredNumber: number, requiredObject: Object, requiredString: string, requiredNode: number | string | React.Element | Array, requiredElement: React.Element, requiredMessage: Message, requiredEnum: 'News' | 'Photos', requiredUnion: string | number | Message, requiredArrayOf: Array, requiredObjectOf: Object, requiredObjectWithShape: { color: string, fontSize: number, }, }; function Button(props: ButtonProps) { return ( ); } export type Button2Props = { requiredArray: Array }; function Button2(props: Button2Props) { const { requiredArray } = props; return ( ); } " `; exports[`React.PropTypes to flow transforms required PropTypes with no \`React\` prefix 1`] = ` " /* @flow */ import React from 'react'; export type Props = { requiredArray: Array, requiredBool: boolean, requiredFunc: Function, requiredNumber: number, requiredObject: Object, requiredString: string, requiredNode: number | string | React.Element | Array, requiredElement: React.Element, requiredMessage: Message, requiredEnum: 'News' | 'Photos', requiredUnion: string | number | Message, requiredArrayOf: Array, requiredObjectOf: Object, requiredObjectWithShape: { color: string, fontSize: number, }, }; export function Button(props: Props) { return ( ); } " `; exports[`React.PropTypes to flow transforms something that just looks like React class 1`] = ` " /* @flow */ import React from 'react'; import PureComponent from '../PureComponent'; type Props = { optionalArray?: Array }; class Test extends PureComponent { props: Props; render() { return (
); } } export default Test; " `; ================================================ FILE: __tests__/proptypes-to-flow-test.js ================================================ /* eslint-env jest */ const jscodeshift = require('jscodeshift'); const transform = require('../src/index').default; const transformString = (source, path = 'test.js') => { return transform({ path, source }, { jscodeshift }, {}); }; describe('React.PropTypes to flow', () => { it('transforms optional PropTypes prefixed with `React`', () => { const input = ` import React from 'react'; export const F = (props) =>
; F.propTypes = { optionalArray: React.PropTypes.array, optionalBool: React.PropTypes.bool, optionalFunc: React.PropTypes.func, optionalNumber: React.PropTypes.number, optionalObject: React.PropTypes.object, optionalString: React.PropTypes.string, optionalNode: React.PropTypes.node, optionalElement: React.PropTypes.element, optionalMessage: React.PropTypes.instanceOf(Message), optionalEnum: React.PropTypes.oneOf(['News', 'Photos']), optionalUnion: React.PropTypes.oneOfType([ React.PropTypes.string, React.PropTypes.number, React.PropTypes.instanceOf(Message) ]), optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number), optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number), optionalObjectWithShape: React.PropTypes.shape({ color: React.PropTypes.string, fontSize: React.PropTypes.number }), }; `; expect(transformString(input)).toMatchSnapshot(); }); it('transforms required PropTypes prefixed with `React`', () => { const input = ` /* eslint */ import React from 'react'; function Button(props) { return ( ); } Button.propTypes = { requiredArray: React.PropTypes.array.isRequired, requiredBool: React.PropTypes.bool.isRequired, requiredFunc: React.PropTypes.func.isRequired, requiredNumber: React.PropTypes.number.isRequired, requiredObject: React.PropTypes.object.isRequired, requiredString: React.PropTypes.string.isRequired, requiredNode: React.PropTypes.node.isRequired, requiredElement: React.PropTypes.element.isRequired, requiredMessage: React.PropTypes.instanceOf(Message).isRequired, requiredEnum: React.PropTypes.oneOf(['News', 'Photos']).isRequired, requiredUnion: React.PropTypes.oneOfType([ React.PropTypes.string, React.PropTypes.number, React.PropTypes.instanceOf(Message) ]).isRequired, requiredArrayOf: React.PropTypes.arrayOf(React.PropTypes.number).isRequired, requiredObjectOf: React.PropTypes.objectOf(React.PropTypes.number).isRequired, requiredObjectWithShape: React.PropTypes.shape({ color: React.PropTypes.string.isRequired, fontSize: React.PropTypes.number.isRequired, }).isRequired, }; function Button2({ requiredArray }) { return ( ); } Button2.propTypes = { requiredArray: React.PropTypes.array.isRequired, }; `; expect(transformString(input)).toMatchSnapshot(); }); it('transforms optional PropTypes with no `React` prefix', () => { const input = ` import React, { PropTypes } from 'react'; function Button(props) { return ( ); } Button.propTypes = { optionalArray: PropTypes.array, optionalBool: PropTypes.bool, optionalFunc: PropTypes.func, optionalNumber: PropTypes.number, optionalObject: PropTypes.object, optionalString: PropTypes.string, optionalNode: PropTypes.node, optionalElement: PropTypes.element, optionalMessage: PropTypes.instanceOf(Message), optionalEnum: PropTypes.oneOf(['News', 'Photos']), optionalUnion: PropTypes.oneOfType([ PropTypes.string, PropTypes.number, PropTypes.instanceOf(Message) ]), optionalArrayOf: PropTypes.arrayOf(PropTypes.number), optionalObjectOf: PropTypes.objectOf(PropTypes.number), optionalObjectWithShape: PropTypes.shape({ color: PropTypes.string, fontSize: PropTypes.number }), }; `; expect(transformString(input)).toMatchSnapshot(); }); it("removes react's 16 PropTypes import", () => { const input = ` import React from 'react'; import PropTypes from 'prop-types'; function Button(props) { return ( ); } Button.propTypes = { optionalArray: PropTypes.array, optionalBool: PropTypes.bool, }; `; expect(transformString(input)).toMatchSnapshot(); }); it("removes react's 16 destructured PropTypes import", () => { const input = ` import React from 'react'; import { bool, array } from 'prop-types'; function Button(props) { return ( ); } Button.propTypes = { optionalArray: PropTypes.array, }; `; expect(transformString(input)).toMatchSnapshot(); }); it('transforms required PropTypes with no `React` prefix', () => { const input = ` import React, { PropTypes } from 'react'; export function Button(props) { return ( ); } Button.propTypes = { requiredArray: PropTypes.array.isRequired, requiredBool: PropTypes.bool.isRequired, requiredFunc: PropTypes.func.isRequired, requiredNumber: PropTypes.number.isRequired, requiredObject: PropTypes.object.isRequired, requiredString: PropTypes.string.isRequired, requiredNode: PropTypes.node.isRequired, requiredElement: PropTypes.element.isRequired, requiredMessage: PropTypes.instanceOf(Message).isRequired, requiredEnum: PropTypes.oneOf(['News', 'Photos']).isRequired, requiredUnion: PropTypes.oneOfType([ PropTypes.string, PropTypes.number, PropTypes.instanceOf(Message) ]).isRequired, requiredArrayOf: PropTypes.arrayOf(PropTypes.number).isRequired, requiredObjectOf: PropTypes.objectOf(PropTypes.number).isRequired, requiredObjectWithShape: PropTypes.shape({ color: PropTypes.string.isRequired, fontSize: PropTypes.number.isRequired, }).isRequired, }; `; expect(transformString(input)).toMatchSnapshot(); }); it('transforms PropTypes that are a class property', () => { const input = ` import React from 'react'; export default class Test extends React.Component { static propTypes = { optionalArray: React.PropTypes.array, optionalBool: React.PropTypes.bool, optionalFunc: React.PropTypes.func, optionalNumber: React.PropTypes.number, optionalObject: React.PropTypes.object, optionalString: React.PropTypes.string, optionalNode: React.PropTypes.node, optionalElement: React.PropTypes.element, optionalMessage: React.PropTypes.instanceOf(Message), optionalEnum: React.PropTypes.oneOf(['News', 'Photos']), optionalUnion: React.PropTypes.oneOfType([ React.PropTypes.string, React.PropTypes.number, React.PropTypes.instanceOf(Message) ]), optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number), optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number), optionalObjectWithShape: React.PropTypes.shape({ color: React.PropTypes.string, fontSize: React.PropTypes.number }), requiredFunc: React.PropTypes.func.isRequired, requiredAny: React.PropTypes.any.isRequired, }; constructor(props) { super(props); } } `; expect(transformString(input)).toMatchSnapshot(); }); it('transforms PropTypes that are defined outside of class definition', () => { const input = ` import React from 'react'; export default class Test extends React.Component { componentDidMount() { } } Test.propTypes = { optionalArray: React.PropTypes.array, optionalBool: React.PropTypes.bool, optionalFunc: React.PropTypes.func, optionalNumber: React.PropTypes.number, optionalObject: React.PropTypes.object, optionalString: React.PropTypes.string, optionalNode: React.PropTypes.node, optionalElement: React.PropTypes.element, optionalMessage: React.PropTypes.instanceOf(Message), optionalEnum: React.PropTypes.oneOf(['News', 'Photos']), optionalUnion: React.PropTypes.oneOfType([ React.PropTypes.string, React.PropTypes.number, React.PropTypes.instanceOf(Message) ]), optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number), optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number), optionalObjectWithShape: React.PropTypes.shape({ color: React.PropTypes.string, fontSize: React.PropTypes.number }), requiredFunc: React.PropTypes.func.isRequired, requiredAny: React.PropTypes.any.isRequired, }; `; expect(transformString(input)).toMatchSnapshot(); }); it('adds type annotation to `prop` parameter in constructor (ES2015)', () => { const input = ` /* @flow */ import React from 'react'; export default class Component extends React.Component { constructor(props) { super(props); } componentDidMount() { } } class Component2 extends React.Component { constructor(props) { super(props); } componentDidMount() { } } `; expect(transformString(input)).toMatchSnapshot(); }); it('preserves comments', () => { const input = ` import React from 'react'; function Button(props) { return ( ); } Button.propTypes = { // You can declare that a prop is a specific JS primitive. By default, these // are all optional. optionalArray: React.PropTypes.array, optionalBool: React.PropTypes.bool, optionalFunc: React.PropTypes.func, optionalNumber: React.PropTypes.number, optionalObject: React.PropTypes.object, optionalString: React.PropTypes.string, // Anything that can be rendered: numbers, strings, elements or an array // (or fragment) containing these types. optionalNode: React.PropTypes.node, // A React element. optionalElement: React.PropTypes.element, // You can also declare that a prop is an instance of a class. This uses // JS's instanceof operator. optionalMessage: React.PropTypes.instanceOf(Message), // You can ensure that your prop is limited to specific values by treating // it as an enum. optionalEnum: React.PropTypes.oneOf(['News', 'Photos']), // An object that could be one of many types optionalUnion: React.PropTypes.oneOfType([ React.PropTypes.string, React.PropTypes.number, React.PropTypes.instanceOf(Message) ]), // An array of a certain type optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number), // An object with property values of a certain type optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number), // An object taking on a particular shape optionalObjectWithShape: React.PropTypes.shape({ color: React.PropTypes.string, fontSize: React.PropTypes.number }), // You can chain any of the above with \`isRequired\` to make sure a warning // is shown if the prop isn't provided. requiredFunc: React.PropTypes.func.isRequired, // A value of any data type requiredAny: React.PropTypes.any.isRequired, }; `; expect(transformString(input)).toMatchSnapshot(); }); it('add empty PropTypes (no constructor)', () => { const input = ` import React from 'react'; import { View } from 'react-native'; class Cards extends React.Component { render() { return ( ); } } export default Cards; `; expect(transformString(input)).toMatchSnapshot(); }); it('adds empty PropTypes (constructor)', () => { const input = ` import { Component } from 'react'; import { View } from 'react-native'; import PureRenderMixin from 'react-addons-pure-render-mixin'; class PureComponent extends Component { constructor(props) { super(props); this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); } render() { return ( ); } } export default PureComponent; `; expect(transformString(input)).toMatchSnapshot(); }); it('does not touch non React classes', () => { const input = ` class PureComponent extends Class { constructor() { } } export default PureComponent; `; expect(transformString(input)).toMatchSnapshot(); }); it('transforms something that just looks like React class', () => { const input = ` import React from 'react'; import PureComponent from '../PureComponent'; class Test extends PureComponent { render() { return (
); } } Test.propTypes = { optionalArray: React.PropTypes.array, }; export default Test; `; expect(transformString(input)).toMatchSnapshot(); }); it('handles functional components with expression body', () => { const input = ` import React, { PropTypes } from 'react'; const MyComponent = ({ hello }) =>
{hello}
; MyComponent.propTypes = { hello: PropTypes.string.isRequired, }; export default MyComponent; `; expect(transformString(input)).toMatchSnapshot(); }); it('handles block comments', () => { const input = ` import React from 'react'; export default class Test extends React.Component { static propTypes = { /** * block comment */ optionalArray: React.PropTypes.array, anotherProp: React.PropTypes.string, }; } `; expect(transformString(input)).toMatchSnapshot(); }); it('handles presence of defaultProps', () => { const input = ` import React from 'react'; export default class Test extends React.Component { static propTypes = { /** * block comment */ optionalArray: React.PropTypes.array, anotherProp: React.PropTypes.string, }; static defaultProps = { anotherProp: '' }; } `; const output = transformString(input); expect(output).toContain('type Props ='); expect(output).toMatchSnapshot(); }); it('does not touch files with flow Props already declared', () => { const input = ` /* @flow */ import React from 'react'; export type Props = { created_at?: string, }; class MyComponent extends React.Component { props: Props; render() { return (
); } } export default MyComponent; `; expect(transformString(input)).toMatchSnapshot(); }); }); ================================================ FILE: package.json ================================================ { "name": "codemod-proptypes-to-flow", "version": "0.0.1", "description": "A codemod to use Flowtype instead of React.PropTypes", "main": "lib/index.js", "scripts": { "build": "babel src --out-dir lib", "check": "npm run lint:bail && npm run test:coverage", "prepublishOnly": "npm run check && npm run build", "precommit": "lint-staged", "lint:bail": "eslint src __tests__", "lint": "eslint src __tests__ --fix", "test": "jest", "test:coverage": "jest --coverage", "test:watch": "jest --watch" }, "repository": { "type": "git", "url": "git+https://github.com/billyvg/codemod-proptypes-to-flow.git" }, "keywords": [ "codemod", "react", "flow", "flowtype", "jscodeshift" ], "author": "Billy Vong ", "license": "MIT", "bugs": { "url": "https://github.com/billyvg/codemod-proptypes-to-flow/issues" }, "homepage": "https://github.com/billyvg/codemod-proptypes-to-flow#readme", "devDependencies": { "babel-cli": "^6.18.0", "babel-eslint": "^6.0.2", "babel-jest": "^17.0.2", "babel-preset-es2015": "^6.18.0", "babel-preset-stage-0": "^6.16.0", "eslint": "^3.9.1", "eslint-config-airbnb-base": "^10.0.1", "eslint-config-prettier": "^2.1.1", "eslint-plugin-import": "^2.2.0", "eslint-plugin-prettier": "^2.1.2", "husky": "^0.13.3", "jest": "^19.0.2", "jscodeshift": "^0.3.30", "lint-staged": "^3.4.1", "prettier": "^1.3.1" }, "lint-staged": { "*.js": [ "eslint --fix", "npm run test --bail --findRelatedTests" ] }, "jest": { "collectCoverageFrom": [ "src/**/*.js" ], "coverageReporters": [ "json", "text", "html" ], "coveragePathIgnorePatterns": [ "/node_modules/", "/__tests__/" ], "testEnvironment": "node" } } ================================================ FILE: src/helpers/ReactUtils.js ================================================ // Origin: https://github.com/reactjs/react-codemod/blob/master/transforms/utils/ReactUtils.js /* eslint-disable func-names */ /** * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * */ module.exports = function(j) { const REACT_CREATE_CLASS_MEMBER_EXPRESSION = { type: 'MemberExpression', object: { name: 'React', }, property: { name: 'createClass', }, }; // --------------------------------------------------------------------------- // Checks if the file requires a certain module const hasModule = (path, module) => path .findVariableDeclarators() .filter(j.filters.VariableDeclarator.requiresModule(module)) .size() === 1 || path .find(j.ImportDeclaration, { type: 'ImportDeclaration', source: { type: 'Literal', }, }) .filter(declarator => declarator.value.source.value === module) .size() === 1; const hasReact = path => hasModule(path, 'React') || hasModule(path, 'react') || hasModule(path, 'react/addons') || hasModule(path, 'react-native'); // --------------------------------------------------------------------------- // Finds all variable declarations that call React.createClass const findReactCreateClassCallExpression = path => j(path).find(j.CallExpression, { callee: REACT_CREATE_CLASS_MEMBER_EXPRESSION, }); const findReactCreateClass = path => path .findVariableDeclarators() .filter(decl => findReactCreateClassCallExpression(decl).size() > 0); const findReactCreateClassExportDefault = path => path.find(j.ExportDeclaration, { default: true, declaration: { type: 'CallExpression', callee: REACT_CREATE_CLASS_MEMBER_EXPRESSION, }, }); const findReactCreateClassModuleExports = path => path.find(j.AssignmentExpression, { left: { type: 'MemberExpression', object: { type: 'Identifier', name: 'module', }, property: { type: 'Identifier', name: 'exports', }, }, right: { type: 'CallExpression', callee: REACT_CREATE_CLASS_MEMBER_EXPRESSION, }, }); const getReactCreateClassSpec = classPath => { const { value } = classPath; const args = (value.init || value.right || value.declaration).arguments; if (args && args.length) { const spec = args[0]; if (spec.type === 'ObjectExpression' && Array.isArray(spec.properties)) { return spec; } } return null; }; // --------------------------------------------------------------------------- // Finds alias for React.Component if used as named import. const findReactComponentName = path => { const reactImportDeclaration = path .find(j.ImportDeclaration, { type: 'ImportDeclaration', source: { type: 'Literal', }, }) .filter(importDeclaration => hasReact(path)); const componentImportSpecifier = reactImportDeclaration .find(j.ImportSpecifier, { type: 'ImportSpecifier', imported: { type: 'Identifier', name: 'Component', }, }) .at(0); const paths = componentImportSpecifier.paths(); return paths.length ? paths[0].value.local.name : undefined; }; // Finds all classes that extend React.Component const findReactES6ClassDeclaration = path => { const componentImport = findReactComponentName(path); const selector = componentImport ? { superClass: { type: 'Identifier', name: componentImport, }, } : { superClass: { type: 'MemberExpression', object: { type: 'Identifier', name: 'React', }, property: { type: 'Identifier', name: 'Component', }, }, }; return path.find(j.ClassDeclaration, selector); }; // --------------------------------------------------------------------------- // Checks if the React class has mixins const isMixinProperty = property => { const key = property.key; const value = property.value; return ( key.name === 'mixins' && value.type === 'ArrayExpression' && Array.isArray(value.elements) && value.elements.length ); }; const hasMixins = classPath => { const spec = getReactCreateClassSpec(classPath); return spec && spec.properties.some(isMixinProperty); }; // --------------------------------------------------------------------------- // Others const getClassExtendReactSpec = classPath => classPath.value.body; const createCreateReactClassCallExpression = properties => j.callExpression( j.memberExpression( j.identifier('React'), j.identifier('createClass'), false ), [j.objectExpression(properties)] ); const getComponentName = classPath => classPath.node.id && classPath.node.id.name; // --------------------------------------------------------------------------- // Direct methods! (see explanation below) const findAllReactCreateClassCalls = path => path.find(j.CallExpression, { callee: REACT_CREATE_CLASS_MEMBER_EXPRESSION, }); // Mixin Stuff const containSameElements = (ls1, ls2) => { if (ls1.length !== ls2.length) { return false; } return ( ls1.reduce((res, x) => res && ls2.indexOf(x) !== -1, true) && ls2.reduce((res, x) => res && ls1.indexOf(x) !== -1, true) ); }; const keyNameIsMixins = property => property.key.name === 'mixins'; const isSpecificMixinsProperty = (property, mixinIdentifierNames) => { const key = property.key; const value = property.value; return ( key.name === 'mixins' && value.type === 'ArrayExpression' && Array.isArray(value.elements) && value.elements.every(elem => elem.type === 'Identifier') && containSameElements( value.elements.map(elem => elem.name), mixinIdentifierNames ) ); }; // These following methods assume that the argument is // a `React.createClass` call expression. In other words, // they should only be used with `findAllReactCreateClassCalls`. const directlyGetCreateClassSpec = classPath => { if (!classPath || !classPath.value) { return null; } const args = classPath.value.arguments; if (args && args.length) { const spec = args[0]; if (spec.type === 'ObjectExpression' && Array.isArray(spec.properties)) { return spec; } } return null; }; const directlyGetComponentName = classPath => { let result = ''; if ( classPath.parentPath.value && classPath.parentPath.value.type === 'VariableDeclarator' ) { result = classPath.parentPath.value.id.name; } return result; }; const directlyHasMixinsField = classPath => { const spec = directlyGetCreateClassSpec(classPath); return spec && spec.properties.some(keyNameIsMixins); }; const directlyHasSpecificMixins = (classPath, mixinIdentifierNames) => { const spec = directlyGetCreateClassSpec(classPath); return ( spec && spec.properties.some(prop => isSpecificMixinsProperty(prop, mixinIdentifierNames) ) ); }; return { createCreateReactClassCallExpression, findReactES6ClassDeclaration, findReactCreateClass, findReactCreateClassCallExpression, findReactCreateClassModuleExports, findReactCreateClassExportDefault, getComponentName, getReactCreateClassSpec, getClassExtendReactSpec, hasMixins, hasModule, hasReact, isMixinProperty, // "direct" methods findAllReactCreateClassCalls, directlyGetComponentName, directlyGetCreateClassSpec, directlyHasMixinsField, directlyHasSpecificMixins, }; }; ================================================ FILE: src/helpers/annotateConstructor.js ================================================ /** * Annotates ES2015 Class constructor and Class `props` member * * @param {jscodeshiftApi} j jscodeshift API * @param {Array} body Array of `Node` */ export default function annotateConstructor(j, body, name = 'Props') { let constructorIndex; const typeAnnotation = j.typeAnnotation( j.genericTypeAnnotation(j.identifier(name), null) ); body.some((b, i) => { if (b.kind === 'constructor') { constructorIndex = i + 1; // first parameter is always props regardless of name if (b.value.params && b.value.params.length) { b.value.params[0].typeAnnotation = typeAnnotation; } return true; } }); body.splice( constructorIndex, 0, j.classProperty(j.identifier('props'), null, typeAnnotation) ); } ================================================ FILE: src/helpers/createTypeAlias.js ================================================ export default function createTypeAlias( j, flowTypes, { name = 'Props', shouldExport = false } = {} ) { const typeAlias = j.typeAlias( j.identifier(name), null, j.objectTypeAnnotation(flowTypes) ); if (shouldExport) { return j.exportNamedDeclaration(typeAlias); } return typeAlias; } ================================================ FILE: src/helpers/findIndex.js ================================================ export default function findIndex(arr, f) { let index; arr.some((val, i) => { const result = f(val, i); if (result) { index = i; } return result; }); return index; } ================================================ FILE: src/helpers/findParentBody.js ================================================ export default function findParentBody(p, memo) { if (p.parentPath) { if (p.parentPath.name === 'body') { return { child: p.value, body: p.parentPath, }; } return findParentBody(p.parentPath); } } ================================================ FILE: src/helpers/propTypeToFlowType.js ================================================ /** * Handles transforming a React.PropType to an equivalent flowtype */ export default function propTypeToFlowType(j, key, value) { /** * Returns an expression without `isRequired` * @param {Node} node NodePath Should be the `value` of a `Property` * @return {Object} Object with `required`, and `node` */ const getExpressionWithoutRequired = inputNode => { // check if it's required let required = false; let node = inputNode; if (inputNode.property && inputNode.property.name === 'isRequired') { required = true; node = inputNode.object; } return { required, node, }; }; /** * Gets the PropType MemberExpression without `React` namespace */ const getPropTypeExpression = inputNode => { if ( inputNode.object && inputNode.object.object && inputNode.object.object.name === 'React' ) { return j.memberExpression(inputNode.object.property, inputNode.property); } else if (inputNode.object && inputNode.object.name === 'React') { return inputNode.property; } return inputNode; }; const TRANSFORM_MAP = { any: j.anyTypeAnnotation(), bool: j.booleanTypeAnnotation(), func: j.genericTypeAnnotation(j.identifier('Function'), null), number: j.numberTypeAnnotation(), object: j.genericTypeAnnotation(j.identifier('Object'), null), string: j.stringTypeAnnotation(), str: j.stringTypeAnnotation(), array: j.genericTypeAnnotation( j.identifier('Array'), j.typeParameterInstantiation([j.anyTypeAnnotation()]) ), element: j.genericTypeAnnotation( j.qualifiedTypeIdentifier(j.identifier('React'), j.identifier('Element')), null ), node: j.unionTypeAnnotation([ j.numberTypeAnnotation(), j.stringTypeAnnotation(), j.genericTypeAnnotation( j.qualifiedTypeIdentifier( j.identifier('React'), j.identifier('Element') ), null ), j.genericTypeAnnotation( j.identifier('Array'), j.typeParameterInstantiation([j.anyTypeAnnotation()]) ), ]), }; let returnValue; const expressionWithoutRequired = getExpressionWithoutRequired(value); const required = expressionWithoutRequired.required; const node = expressionWithoutRequired.node; // Check for React namespace for MemberExpressions (i.e. React.PropTypes.string) if (node.object) { node.object = getPropTypeExpression(node.object); } else if (node.callee) { node.callee = getPropTypeExpression(node.callee); } if (node.type === 'Literal') { returnValue = j.stringLiteralTypeAnnotation(node.value, node.raw); } else if (node.type === 'MemberExpression') { returnValue = TRANSFORM_MAP[node.property.name]; } else if (node.type === 'CallExpression') { // instanceOf(), arrayOf(), etc.. const name = node.callee.property.name; if (name === 'instanceOf') { returnValue = j.genericTypeAnnotation(node.arguments[0], null); } else if (name === 'arrayOf') { returnValue = j.genericTypeAnnotation( j.identifier('Array'), j.typeParameterInstantiation([ propTypeToFlowType( j, null, node.arguments[0] || j.anyTypeAnnotation() ), ]) ); } else if (name === 'objectOf') { // TODO: Is there a direct Flow translation for this? returnValue = j.genericTypeAnnotation( j.identifier('Object'), j.typeParameterInstantiation([ propTypeToFlowType( j, null, node.arguments[0] || j.anyTypeAnnotation() ), ]) ); } else if (name === 'shape') { returnValue = j.objectTypeAnnotation( node.arguments[0].properties.map(arg => propTypeToFlowType(j, arg.key, arg.value) ) ); } else if (name === 'oneOfType' || name === 'oneOf') { returnValue = j.unionTypeAnnotation( node.arguments[0].elements.map(arg => propTypeToFlowType(j, null, arg)) ); } } else if (node.type === 'ObjectExpression') { returnValue = j.objectTypeAnnotation( node.arguments.map(arg => propTypeToFlowType(j, arg.key, arg.value)) ); } else if (node.type === 'Identifier') { returnValue = j.genericTypeAnnotation(node, null); } // finally return either an objectTypeProperty or just a property if `key` is null if (!key) { return returnValue; } else { return j.objectTypeProperty(key, returnValue, !required); } } ================================================ FILE: src/helpers/removePropTypeImport.js ================================================ export default function removePropTypeImport(j, ast) { // remove `PropTypes` from import React, { PropTypes } from 'react' ast .find(j.ImportDeclaration, { type: 'ImportDeclaration', source: { type: 'Literal', value: 'react', }, }) .find(j.ImportSpecifier, { imported: { name: 'PropTypes' } }) .remove(); // remove whole line import { PropTypes } from 'react' ast .find(j.ImportDeclaration, { type: 'ImportDeclaration', source: { type: 'Literal', value: 'react', }, }) .filter(p => p.value.specifiers.length === 0) .remove(); // remove react16 import PropType from 'prop-types' or import { bool } from 'prop-types' ast .find(j.ImportDeclaration, { type: 'ImportDeclaration', source: { type: 'Literal', value: 'prop-types', }, }) .remove(); } ================================================ FILE: src/helpers/transformProperties.js ================================================ import propTypeToFlowType from './propTypeToFlowType'; export default function transformProperties(j, properties) { return properties.map(property => { const type = propTypeToFlowType(j, property.key, property.value); type.leadingComments = property.leadingComments; type.comments = property.comments; return type; }); } ================================================ FILE: src/index.js ================================================ import transformEs6ClassComponents from './transformers/es6Classes'; import transformFunctionalComponents from './transformers/functional'; import ReactUtils from './helpers/ReactUtils'; function addFlowComment(j, ast, options) { const getBodyNode = () => ast.find(j.Program).get('body', 0).node; const comments = getBodyNode().comments || []; const containsFlowComment = comments.filter(e => e.value.indexOf('@flow') !== -1).length > 0; if (!containsFlowComment) { switch (options.flowComment) { case 'line': comments.unshift(j.commentLine(' @flow')); break; case 'block': default: comments.unshift(j.commentBlock(' @flow ')); break; } } getBodyNode().comments = comments; } export default function transformer(file, api, rawOptions) { const j = api.jscodeshift; const root = j(file.source); const options = rawOptions; if (options.flowComment !== 'line' && options.flowComment !== 'block') { if (options.flowComment) { console.warn( `Unsupported flowComment value provided: ${options.flowComment}` ); console.warn('Supported options are "block" and "line".'); console.warn('Falling back to default: "block".'); } options.flowComment = 'block'; } if (!options.propsTypeSuffix) { options.propsTypeSuffix = 'Props'; } const reactUtils = ReactUtils(j); if (!reactUtils.hasReact(root)) { return file.source; } const classModifications = transformEs6ClassComponents(root, j, options); const functionalModifications = transformFunctionalComponents( root, j, options ); if (classModifications || functionalModifications) { addFlowComment(j, root, options); return root.toSource({ quote: 'single', trailingComma: true }); } else { return file.source; } } ================================================ FILE: src/transformers/es6Classes.js ================================================ import annotateConstructor from '../helpers/annotateConstructor'; import createTypeAlias from '../helpers/createTypeAlias'; import findIndex from '../helpers/findIndex'; import findParentBody from '../helpers/findParentBody'; import transformProperties from '../helpers/transformProperties'; import ReactUtils from '../helpers/ReactUtils'; import removePropTypeImportDeclaration from '../helpers/removePropTypeImport'; const isStaticPropType = p => { return ( p.type === 'ClassProperty' && p.static && p.key.type === 'Identifier' && p.key.name === 'propTypes' ); }; function containsFlowProps(classBody) { return !!classBody.find(bodyElement => bodyElement.key.name === 'props'); } /** * Transforms es2016 components * @return true if any components were transformed. */ export default function transformEs6Classes(ast, j, options) { const reactUtils = ReactUtils(j); const classNamesWithPropsOutside = []; // NOTE: reactUtils.findReactES6ClassDeclaration(ast) is missing extends // for local imported components... If finding all classes is too greety, // we might combine findReactES6ClassDeclaration with classes that have a // render method. const reactClassPaths = ast.find(j.ClassDeclaration); // find classes with propType static class property const modifications = reactClassPaths .forEach(p => { const className = reactUtils.getComponentName(p); const propIdentifier = reactClassPaths.length === 1 ? options.propsTypeSuffix : `${className}${options.propsTypeSuffix}`; let properties; const classBody = p.value.body && p.value.body.body; if (classBody) { if (containsFlowProps(classBody)) { return; } annotateConstructor(j, classBody, propIdentifier); const index = findIndex(classBody, isStaticPropType); if (typeof index !== 'undefined') { const classProperty = classBody.splice(index, 1).pop(); properties = classProperty.value.properties; } else { // look for propTypes defined elsewhere classNamesWithPropsOutside.push(className); ast .find(j.AssignmentExpression, { left: { type: 'MemberExpression', object: { name: className, }, property: { name: 'propTypes', }, }, right: { type: 'ObjectExpression', }, }) .forEach(p => { // this should only be one? properties = p.value.right.properties; }) .remove(); } properties = properties || []; const typeAlias = createTypeAlias( j, transformProperties(j, properties), { name: propIdentifier, shouldExport: false, } ); // Find location to put propTypes flowtype definition // This will place ahead of class def const { child, body } = findParentBody(p); if (body && child) { const bodyIndex = findIndex(body.value, b => b === child); if (bodyIndex) { body.value.splice(bodyIndex, 0, typeAlias); } } } }) .size(); ast .find(j.ExpressionStatement, { expression: { type: 'AssignmentExpression', left: { type: 'MemberExpression', property: { name: 'propTypes', }, }, right: { type: 'ObjectExpression', }, }, }) .filter( p => classNamesWithPropsOutside.indexOf( p.value.expression.left.object.name ) > -1 ) .remove(); removePropTypeImportDeclaration(j, ast); return modifications > 0; } ================================================ FILE: src/transformers/functional.js ================================================ import propTypeToFlowType from '../helpers/propTypeToFlowType'; function removeComponentAssignmentPropTypes(ast, j) { const componentToPropTypesRemoved = {}; ast .find(j.AssignmentExpression, { left: { property: { name: 'propTypes', }, }, }) .forEach(p => { const objectName = p.value.left.object.name; const properties = p.value.right.properties; const flowTypesRemoved = properties.map(property => { const t = propTypeToFlowType(j, property.key, property.value); t.comments = property.comments; return t; }); componentToPropTypesRemoved[objectName] = flowTypesRemoved; }) .remove(); return componentToPropTypesRemoved; } function insertTypeIdentifierInFunction(functionPath, j, typeIdentifier) { const functionRoot = functionPath.value.init || functionPath.value; const params = functionRoot.params; const param = params[0]; const newTypeAnnotation = j.typeAnnotation( j.genericTypeAnnotation(j.identifier(typeIdentifier), null) ); if (param.type === 'Identifier') { param.typeAnnotation = newTypeAnnotation; } else if (param.type === 'ObjectPattern') { // NOTE: something is wrong with recast and objectPatterns... // You cannot set typeAnnotation on them, do object spread instead const newProps = j.identifier('props'); newProps.typeAnnotation = newTypeAnnotation; functionRoot.params = [newProps]; const newSpread = j.variableDeclaration('const', [ j.variableDeclarator(param, j.identifier('props')), ]); // if the body of the function is an expression, we need to construct // a block statement to hold the props spread if (functionRoot.body.type === 'BlockStatement') { functionRoot.body.body.unshift(newSpread); } else { const returnExpression = j.returnStatement(functionRoot.body); functionRoot.body = j.blockStatement([newSpread, returnExpression]); } } } /** * Transforms function components * @return true if any functional components were transformed. */ export default function transformFunctionalComponents(ast, j, options) { // Look for Foo.propTypes const componentToPropTypesRemoved = removeComponentAssignmentPropTypes( ast, j ); const components = Object.keys(componentToPropTypesRemoved); if (components.length === 0) { return null; } components.forEach(c => { const flowTypesRemoved = componentToPropTypesRemoved[c]; const propIdentifier = components.length === 1 ? options.propsTypeSuffix : `${c}${options.propsTypeSuffix}`; const flowTypeProps = j.exportNamedDeclaration( j.typeAlias( j.identifier(propIdentifier), null, j.objectTypeAnnotation(flowTypesRemoved) ) ); ast .find(j.FunctionDeclaration, { id: { name: c }, }) .forEach(f => { const insertNode = f.parent.node.type === 'Program' ? f : f.parent; insertNode.insertBefore(flowTypeProps); insertTypeIdentifierInFunction(f, j, propIdentifier); }); ast .find(j.VariableDeclarator, { id: { name: c }, }) .forEach(f => { const insertNode = f.parent.parent.node.type === 'Program' ? f.parent : f.parent.parent; insertNode.insertBefore(flowTypeProps); insertTypeIdentifierInFunction(f, j, propIdentifier); }); }); return components.length > 0; }