├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── package.json ├── src ├── cli.ts ├── compiler.ts ├── helpers │ ├── build-prop-type-interface.ts │ └── index.ts ├── index.ts ├── transforms │ ├── collapse-intersection-interfaces-transform.ts │ ├── react-js-make-props-and-state-transform.ts │ ├── react-move-prop-types-to-class-transform.ts │ ├── react-remove-prop-types-assignment-transform.ts │ ├── react-remove-prop-types-import.ts │ ├── react-remove-static-prop-types-member-transform.ts │ └── react-stateless-function-make-props-transform.ts └── untyped-modules.d.ts ├── test ├── collapse-intersection-interfaces-transform │ ├── advanced │ │ ├── input.tsx │ │ └── output.tsx │ ├── empty-empty │ │ ├── input.tsx │ │ └── output.tsx │ ├── multiple │ │ ├── input.tsx │ │ └── output.tsx │ ├── repeated │ │ ├── input.tsx │ │ └── output.tsx │ └── simple │ │ ├── input.tsx │ │ └── output.tsx ├── end-to-end │ ├── basic │ │ ├── input.tsx │ │ └── output.tsx │ ├── initial-state-and-proprypes-and-set-state │ │ ├── input.tsx │ │ └── output.tsx │ ├── initial-state-and-proprypes │ │ ├── input.tsx │ │ └── output.tsx │ ├── multiple-components │ │ ├── input.tsx │ │ └── output.tsx │ ├── non-react │ │ ├── input.tsx │ │ └── output.tsx │ ├── stateless-arrow-function │ │ ├── input.tsx │ │ └── output.tsx │ └── stateless-function │ │ ├── input.tsx │ │ └── output.tsx ├── react-js-make-props-and-state-transform │ ├── multiple-components │ │ ├── input.tsx │ │ └── output.tsx │ ├── non-react │ │ ├── input.tsx │ │ └── output.tsx │ ├── propless-stateless │ │ ├── input.tsx │ │ └── output.tsx │ ├── set-state-advanced │ │ ├── input.tsx │ │ └── output.tsx │ ├── set-state-only │ │ ├── input.tsx │ │ └── output.tsx │ ├── state-in-class-member │ │ ├── input.tsx │ │ └── output.tsx │ ├── state-in-constructor │ │ ├── input.tsx │ │ └── output.tsx │ ├── static-proptypes-getter-simple │ │ ├── input.tsx │ │ └── output.tsx │ ├── static-proptypes-many-props │ │ ├── input.tsx │ │ └── output.tsx │ └── static-proptypes-simple │ │ ├── input.tsx │ │ └── output.tsx ├── react-move-prop-types-to-class-transform │ ├── multiple-components │ │ ├── input.tsx │ │ └── output.tsx │ └── simple │ │ ├── input.tsx │ │ └── output.tsx ├── react-remove-prop-types-assignment-transform │ ├── functional-components │ │ ├── input.tsx │ │ └── output.tsx │ ├── multiple │ │ ├── input.tsx │ │ └── output.tsx │ └── simple │ │ ├── input.tsx │ │ └── output.tsx ├── react-remove-prop-types-import │ ├── from-prop-types │ │ ├── input.tsx │ │ └── output.tsx │ ├── from-react-multi-named-import │ │ ├── input.tsx │ │ └── output.tsx │ └── from-react-simple │ │ ├── input.tsx │ │ └── output.tsx ├── react-remove-static-prop-types-member-transform │ ├── getter │ │ ├── input.tsx │ │ └── output.tsx │ ├── multiple-components │ │ ├── input.tsx │ │ └── output.tsx │ ├── multiple │ │ ├── input.tsx │ │ └── output.tsx │ ├── other-static-members │ │ ├── input.tsx │ │ └── output.tsx │ └── simple │ │ ├── input.tsx │ │ └── output.tsx ├── react-stateless-function-make-props-transform │ ├── empty-prop │ │ ├── input.tsx │ │ └── output.tsx │ ├── multiple-components │ │ ├── input.tsx │ │ └── output.tsx │ ├── stateless-arrow-function │ │ ├── input.tsx │ │ └── output.tsx │ ├── stateless-function-many-props │ │ ├── input.tsx │ │ └── output.tsx │ ├── stateless-function │ │ ├── input.tsx │ │ └── output.tsx │ └── stateless-propless │ │ ├── input.tsx │ │ └── output.tsx └── transformers.test.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | coverage/ 5 | *.log 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src 3 | test 4 | .vscode 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | test/ 2 | dist/ 3 | !test/transformers.test.ts 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 120, 4 | "tabWidth": 4, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: stable 3 | cache: yarn 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "args": [], 6 | "cwd": "${workspaceRoot}", 7 | "env": { 8 | "NODE_ENV": "test" 9 | }, 10 | "console": "internalConsole", 11 | "name": "Run Tests", 12 | "outFiles": ["${workspaceRoot}/dist"], 13 | "preLaunchTask": "tsc", 14 | "program": "${workspaceRoot}/node_modules/.bin/jest", 15 | "request": "launch", 16 | "runtimeArgs": [], 17 | "runtimeExecutable": null, 18 | "sourceMaps": true, 19 | "stopOnEntry": false, 20 | "type": "node" 21 | }, 22 | { 23 | "name": "Attach", 24 | "type": "node", 25 | "request": "attach", 26 | "port": 5858 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true, 8 | "**/node_modules": true, 9 | "**/dist": true 10 | }, 11 | "search.exclude": { 12 | "**/node_modules": true, 13 | "**/dist": true 14 | }, 15 | "typescript.tsdk": "node_modules/typescript/lib" 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "tsc", 6 | "isShellCommand": true, 7 | "args": ["-p", "."], 8 | "showOutput": "silent", 9 | "problemMatcher": "$tsc" 10 | } 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | This project is governed by [Lyft's code of conduct](https://github.com/lyft/code-of-conduct). All contributors and participants agree to abide by its terms. 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014 Lyft, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React JavaScript to TypeScript Transform 2 | 3 | [![Build Status](https://travis-ci.org/lyft/react-javascript-to-typescript-transform.svg?branch=master)](https://travis-ci.org/lyft/react-javascript-to-typescript-transform) 4 | 5 | Transforms React code written in JavaScript to TypeScript. 6 | 7 | [**🖥 Download the VSCode Extension**](https://marketplace.visualstudio.com/items?itemName=mohsen1.react-javascript-to-typescript-transform-vscode) 8 | 9 | ## Features: 10 | 11 | * Proxies `PropTypes` to `React.Component` generic type and removes PropTypes 12 | * Provides state typing for `React.Component` based on initial state and `setState()` calls in the component 13 | * Hoist large interfaces for props and state out of `React.Component` into declared types 14 | * Convert functional components with `PropTypes` property to TypeScript and uses propTypes to generate function type declaration 15 | 16 | ## Example 17 | 18 | **input** 19 | 20 | ```jsx 21 | class MyComponent extends React.Component { 22 | static propTypes = { 23 | prop1: React.PropTypes.string.isRequired, 24 | prop2: React.PropTypes.number, 25 | }; 26 | constructor() { 27 | super(); 28 | this.state = { foo: 1, bar: 'str' }; 29 | } 30 | render() { 31 | return ( 32 |
33 | {this.state.foo}, {this.state.bar}, {this.state.baz} 34 |
35 | ); 36 | } 37 | onClick() { 38 | this.setState({ baz: 3 }); 39 | } 40 | } 41 | ``` 42 | 43 | **output** 44 | 45 | ```tsx 46 | type MyComponentProps = { 47 | prop1: string; 48 | prop2?: number; 49 | }; 50 | 51 | type MyComponentState = { 52 | foo: number; 53 | bar: string; 54 | baz: number; 55 | }; 56 | 57 | class MyComponent extends React.Component { 58 | constructor() { 59 | super(); 60 | this.state = { foo: 1, bar: 'str' }; 61 | } 62 | render() { 63 | return ( 64 |
65 | {this.state.foo}, {this.state.bar}, {this.state.baz} 66 |
67 | ); 68 | } 69 | onClick() { 70 | this.setState({ baz: 3 }); 71 | } 72 | } 73 | ``` 74 | 75 | ## Usage 76 | 77 | ### CLI 78 | 79 | ``` 80 | npm install -g react-js-to-ts 81 | ``` 82 | 83 | ``` 84 | react-js-to-ts my-react-js-file.js 85 | ``` 86 | 87 | ### VSCode plugin 88 | 89 | details 90 | [Download from VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=mohsen1.react-javascript-to-typescript-transform-vscode#overview) 91 | 92 | ## Development 93 | 94 | ### Tests 95 | 96 | Tests are organized in `test` folder. For each transform there is a folder that contains folders for each test case. Each test case has `input.tsx` and `output.tsx`. 97 | 98 | ``` 99 | npm test 100 | ``` 101 | 102 | #### Watch mode 103 | 104 | Pass `-w` to `npm test` 105 | 106 | ``` 107 | npm test -- -w 108 | ``` 109 | 110 | #### Only a single test case 111 | 112 | Pass `-t` with transform name and case name space separated to `npm test` 113 | 114 | ``` 115 | npm test -- -t "react-js-make-props-and-state-transform propless-stateless" 116 | ``` 117 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-js-to-ts", 3 | "version": "1.4.0", 4 | "description": "Convert React code from JavaScript to TypeScript", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "pretest": "npm run build", 8 | "test": "jest", 9 | "coverage": "jest --coverage", 10 | "posttest": "npm run lint", 11 | "prelint": "npm run clean", 12 | "lint": "tslint --project tsconfig.json --format codeFrame --exclude test/**/*.tsx", 13 | "prepublish": "npm run build", 14 | "clean": "rm -rf dist", 15 | "prebuild": "npm run clean", 16 | "build": "tsc --pretty", 17 | "precommit": "lint-staged", 18 | "prettier": "prettier --write *.{js,json,css,md,ts,tsx}" 19 | }, 20 | "jest": { 21 | "transform": { 22 | ".ts": "/node_modules/ts-jest/preprocessor.js" 23 | }, 24 | "testRegex": "(/tests/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 25 | "moduleFileExtensions": ["ts", "js"] 26 | }, 27 | "lint-staged": { 28 | "*.{js,json,css,md,ts,tsx}": ["node_modules/.bin/prettier --write", "git add"] 29 | }, 30 | "bin": "dist/cli.js", 31 | "author": "Mohsen Azimi ", 32 | "license": "Apache-2.0", 33 | "dependencies": { 34 | "chalk": "^2.4.1", 35 | "commander": "^2.15.1", 36 | "detect-indent": "^5.0.0", 37 | "glob": "^7.1.2", 38 | "lodash": "^4.17.10", 39 | "prettier": "^1.12.1", 40 | "typescript": "2.8.3" 41 | }, 42 | "devDependencies": { 43 | "@types/chalk": "^2.2.0", 44 | "@types/commander": "^2.9.1", 45 | "@types/detect-indent": "^5.0.0", 46 | "@types/glob": "^5.0.35", 47 | "@types/jest": "^22.2.3", 48 | "@types/lodash": "^4.14.109", 49 | "@types/node": "^10.1.2", 50 | "@types/prettier": "^1.12.2", 51 | "@types/react": "^16.3.14", 52 | "dedent": "^0.7.0", 53 | "husky": "^0.14.3", 54 | "jest": "^22.4.4", 55 | "lint-staged": "^7.1.1", 56 | "ts-jest": "^22.4.6", 57 | "ts-node": "^6.0.3", 58 | "tslint": "^5.10.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as program from 'commander'; 4 | import * as glob from 'glob'; 5 | import * as fs from 'fs'; 6 | import * as path from 'path'; 7 | import * as prettier from 'prettier'; 8 | 9 | import { run } from '.'; 10 | import { CompilationOptions } from './compiler'; 11 | 12 | function resolveGlobs(globPatterns: string[]): string[] { 13 | const files: string[] = []; 14 | function addFile(file: string) { 15 | file = path.resolve(file); 16 | if (files.indexOf(file) === -1) { 17 | files.push(file); 18 | } 19 | } 20 | globPatterns.forEach(pattern => { 21 | if (/[{}*?+\[\]]/.test(pattern)) { 22 | // Smells like globs 23 | glob.sync(pattern, {}).forEach(file => { 24 | addFile(file); 25 | }); 26 | } else { 27 | addFile(pattern); 28 | } 29 | }); 30 | return files; 31 | } 32 | 33 | program 34 | .version('1.0.0') 35 | .option('--arrow-parens ', 'Include parentheses around a sole arrow function parameter.', 'avoid') 36 | .option('--no-bracket-spacing', 'Do not print spaces between brackets.', false) 37 | .option('--jsx-bracket-same-line', 'Put > on the last line instead of at a new line.', false) 38 | .option('--print-width ', 'The line length where Prettier will try wrap.', 80) 39 | .option('--prose-wrap How to wrap prose. (markdown)', 'preserve') 40 | .option('--no-semi', 'Do not print semicolons, except at the beginning of lines which may need them', false) 41 | .option('--single-quote', 'Use single quotes instead of double quotes.', false) 42 | .option('--tab-width ', 'Number of spaces per indentation level.', 2) 43 | .option('--trailing-comma ', 'Print trailing commas wherever possible when multi-line.', 'none') 44 | .option('--use-tabs', 'Indent with tabs instead of spaces.', false) 45 | .option('--ignore-prettier-errors', 'Ignore (but warn about) errors in Prettier', false) 46 | .option('--keep-original-files', 'Keep original files', false) 47 | .option('--keep-temporary-files', 'Keep temporary files', false) 48 | .usage('[options] ') 49 | .command('* [glob/filename...]') 50 | .action((globPatterns: string[]) => { 51 | const prettierOptions: prettier.Options = { 52 | arrowParens: program.arrowParens, 53 | bracketSpacing: !program.noBracketSpacing, 54 | jsxBracketSameLine: !!program.jsxBracketSameLine, 55 | printWidth: parseInt(program.printWidth, 10), 56 | proseWrap: program.proseWrap, 57 | semi: !program.noSemi, 58 | singleQuote: !!program.singleQuote, 59 | tabWidth: parseInt(program.tabWidth, 10), 60 | trailingComma: program.trailingComma, 61 | useTabs: !!program.useTabs, 62 | }; 63 | const compilationOptions: CompilationOptions = { 64 | ignorePrettierErrors: !!program.ignorePrettierErrors, 65 | }; 66 | const files = resolveGlobs(globPatterns); 67 | if (!files.length) { 68 | throw new Error('Nothing to do. You must provide file names or glob patterns to transform.'); 69 | } 70 | let errors = false; 71 | for (const filePath of files) { 72 | console.log(`Transforming ${filePath}...`); 73 | const newPath = filePath.replace(/\.jsx?$/, '.tsx'); 74 | const temporaryPath = filePath.replace(/\.jsx?$/, `_js2ts_${+new Date()}.tsx`); 75 | try { 76 | fs.copyFileSync(filePath, temporaryPath); 77 | const result = run(temporaryPath, prettierOptions, compilationOptions); 78 | fs.writeFileSync(newPath, result); 79 | if (!program.keepOriginalFiles) { 80 | fs.unlinkSync(filePath); 81 | } 82 | } catch (error) { 83 | console.warn(`Failed to convert ${filePath}`); 84 | console.warn(error); 85 | errors = true; 86 | } 87 | if (!program.keepTemporaryFiles) { 88 | if (fs.existsSync(temporaryPath)) { 89 | fs.unlinkSync(temporaryPath); 90 | } 91 | } 92 | } 93 | if (errors) { 94 | process.exit(1); 95 | } 96 | }); 97 | 98 | program.parse(process.argv); 99 | -------------------------------------------------------------------------------- /src/compiler.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | import * as fs from 'fs'; 3 | import * as ts from 'typescript'; 4 | import chalk from 'chalk'; 5 | import * as _ from 'lodash'; 6 | import * as prettier from 'prettier'; 7 | import * as detectIndent from 'detect-indent'; 8 | 9 | import { TransformFactoryFactory } from '.'; 10 | 11 | export interface CompilationOptions { 12 | ignorePrettierErrors: boolean; 13 | } 14 | 15 | const DEFAULT_COMPILATION_OPTIONS: CompilationOptions = { 16 | ignorePrettierErrors: false, 17 | }; 18 | 19 | export { DEFAULT_COMPILATION_OPTIONS }; 20 | 21 | /** 22 | * Compile and return result TypeScript 23 | * @param filePath Path to file to compile 24 | */ 25 | export function compile( 26 | filePath: string, 27 | factoryFactories: TransformFactoryFactory[], 28 | incomingPrettierOptions: prettier.Options = {}, 29 | compilationOptions: CompilationOptions = DEFAULT_COMPILATION_OPTIONS, 30 | ) { 31 | const compilerOptions: ts.CompilerOptions = { 32 | target: ts.ScriptTarget.ES2017, 33 | module: ts.ModuleKind.ES2015, 34 | }; 35 | 36 | const program = ts.createProgram([filePath], compilerOptions); 37 | // `program.getSourceFiles()` will include those imported files, 38 | // like: `import * as a from './file-a'`. 39 | // We should only transform current file. 40 | const sourceFiles = program.getSourceFiles().filter(sf => sf.fileName === filePath); 41 | const typeChecker = program.getTypeChecker(); 42 | 43 | const result = ts.transform( 44 | sourceFiles, 45 | factoryFactories.map(factoryFactory => factoryFactory(typeChecker), compilerOptions), 46 | ); 47 | 48 | if (result.diagnostics && result.diagnostics.length) { 49 | console.log( 50 | chalk.yellow(` 51 | ======================= Diagnostics for ${filePath} ======================= 52 | `), 53 | ); 54 | for (const diag of result.diagnostics) { 55 | if (diag.file && diag.start) { 56 | const pos = diag.file.getLineAndCharacterOfPosition(diag.start); 57 | console.log(`(${pos.line}, ${pos.character}) ${diag.messageText}`); 58 | } 59 | } 60 | } 61 | 62 | const printer = ts.createPrinter(); 63 | 64 | // TODO: fix the index 0 access... What if program have multiple source files? 65 | const printed = printer.printNode(ts.EmitHint.SourceFile, result.transformed[0], sourceFiles[0]); 66 | 67 | const inputSource = fs.readFileSync(filePath, 'utf-8'); 68 | const prettierOptions = getPrettierOptions(filePath, inputSource, incomingPrettierOptions); 69 | 70 | try { 71 | return prettier.format(printed, prettierOptions); 72 | } catch (prettierError) { 73 | if (compilationOptions.ignorePrettierErrors) { 74 | console.warn(`Prettier failed for ${filePath} (ignorePrettierErrors is on):`); 75 | console.warn(prettierError); 76 | return printed; 77 | } 78 | throw prettierError; 79 | } 80 | } 81 | 82 | /** 83 | * Get Prettier options based on style of a JavaScript 84 | * @param filePath Path to source file 85 | * @param source Body of a JavaScript 86 | * @param options Existing prettier option 87 | */ 88 | export function getPrettierOptions(filePath: string, source: string, options: prettier.Options): prettier.Options { 89 | const resolvedOptions = prettier.resolveConfig.sync(filePath); 90 | if (resolvedOptions) { 91 | _.defaults(resolvedOptions, options); 92 | return resolvedOptions; 93 | } 94 | const { amount: indentAmount, type: indentType } = detectIndent(source); 95 | const sourceWidth = getCodeWidth(source, 80); 96 | const semi = getUseOfSemi(source); 97 | const quotations = getQuotation(source); 98 | 99 | _.defaults(Object.assign({}, options), { 100 | tabWidth: indentAmount, 101 | useTabs: indentType && indentType === 'tab', 102 | printWidth: sourceWidth, 103 | semi, 104 | singleQuote: quotations === 'single', 105 | }); 106 | 107 | return options; 108 | } 109 | 110 | /** 111 | * Given body of a source file, return its code width 112 | * @param source 113 | */ 114 | function getCodeWidth(source: string, defaultWidth: number): number { 115 | return source.split(os.EOL).reduce((result, line) => Math.max(result, line.length), defaultWidth); 116 | } 117 | 118 | /** 119 | * Detect if a source file is using semicolon 120 | * @todo: use an actual parser. This is not a proper implementation 121 | * @param source 122 | * @return true if code is using semicolons 123 | */ 124 | function getUseOfSemi(source: string): boolean { 125 | return source.indexOf(';') !== -1; 126 | } 127 | 128 | /** 129 | * Detect if a source file is using single quotes or double quotes 130 | * @todo use an actual parser. This is not a proper implementation 131 | * @param source 132 | */ 133 | function getQuotation(source: string): 'single' | 'double' { 134 | const numberOfSingleQuotes = (source.match(/\'/g) || []).length; 135 | const numberOfDoubleQuotes = (source.match(/\"/g) || []).length; 136 | if (numberOfSingleQuotes > numberOfDoubleQuotes) { 137 | return 'single'; 138 | } 139 | return 'double'; 140 | } 141 | -------------------------------------------------------------------------------- /src/helpers/build-prop-type-interface.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | /** 4 | * Build props interface from propTypes object 5 | * @example 6 | * { 7 | * foo: React.PropTypes.string.isRequired 8 | * } 9 | * 10 | * becomes 11 | * { 12 | * foo: string; 13 | * } 14 | * @param objectLiteral 15 | */ 16 | export function buildInterfaceFromPropTypeObjectLiteral(objectLiteral: ts.ObjectLiteralExpression) { 17 | const members = objectLiteral.properties 18 | // We only need to process PropertyAssignment: 19 | // { 20 | // a: 123 // PropertyAssignment 21 | // } 22 | // 23 | // filter out: 24 | // { 25 | // a() {}, // MethodDeclaration 26 | // b, // ShorthandPropertyAssignment 27 | // ...c, // SpreadAssignment 28 | // get d() {}, // AccessorDeclaration 29 | // } 30 | .filter(ts.isPropertyAssignment) 31 | // Ignore children, React types have it 32 | .filter(property => property.name.getText() !== 'children') 33 | .map(propertyAssignment => { 34 | const name = propertyAssignment.name.getText(); 35 | const initializer = propertyAssignment.initializer; 36 | const isRequired = isPropTypeRequired(initializer); 37 | const typeExpression = isRequired 38 | ? // We have guaranteed the type in `isPropTypeRequired()` 39 | (initializer as ts.PropertyAccessExpression).expression 40 | : initializer; 41 | const typeValue = getTypeFromReactPropTypeExpression(typeExpression); 42 | 43 | return ts.createPropertySignature( 44 | [], 45 | name, 46 | isRequired ? undefined : ts.createToken(ts.SyntaxKind.QuestionToken), 47 | typeValue, 48 | undefined, 49 | ); 50 | }); 51 | 52 | return ts.createTypeLiteralNode(members); 53 | } 54 | 55 | /** 56 | * Turns React.PropTypes.* into TypeScript type value 57 | * 58 | * @param node React propTypes value 59 | */ 60 | function getTypeFromReactPropTypeExpression(node: ts.Expression): ts.TypeNode { 61 | let result = null; 62 | if (ts.isPropertyAccessExpression(node)) { 63 | /** 64 | * PropTypes.array, 65 | * PropTypes.bool, 66 | * PropTypes.func, 67 | * PropTypes.number, 68 | * PropTypes.object, 69 | * PropTypes.string, 70 | * PropTypes.symbol, (ignore) 71 | * PropTypes.node, 72 | * PropTypes.element, 73 | * PropTypes.any, 74 | */ 75 | const text = node.getText().replace(/React\.PropTypes\./, ''); 76 | 77 | if (/string/.test(text)) { 78 | result = ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); 79 | } else if (/any/.test(text)) { 80 | result = ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); 81 | } else if (/array/.test(text)) { 82 | result = ts.createArrayTypeNode(ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); 83 | } else if (/bool/.test(text)) { 84 | result = ts.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword); 85 | } else if (/number/.test(text)) { 86 | result = ts.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword); 87 | } else if (/object/.test(text)) { 88 | result = ts.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword); 89 | } else if (/node/.test(text)) { 90 | result = ts.createTypeReferenceNode('React.ReactNode', []); 91 | } else if (/element/.test(text)) { 92 | result = ts.createTypeReferenceNode('JSX.Element', []); 93 | } else if (/func/.test(text)) { 94 | const arrayOfAny = ts.createParameter( 95 | [], 96 | [], 97 | ts.createToken(ts.SyntaxKind.DotDotDotToken), 98 | 'args', 99 | undefined, 100 | ts.createArrayTypeNode(ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), 101 | undefined, 102 | ); 103 | result = ts.createFunctionTypeNode([], [arrayOfAny], ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); 104 | } 105 | } else if (ts.isCallExpression(node)) { 106 | /** 107 | * PropTypes.instanceOf(), (ignore) 108 | * PropTypes.oneOf(), // only support oneOf([1, 2]), oneOf(['a', 'b']) 109 | * PropTypes.oneOfType(), 110 | * PropTypes.arrayOf(), 111 | * PropTypes.objectOf(), 112 | * PropTypes.shape(), 113 | */ 114 | const text = node.expression.getText(); 115 | if (/oneOf$/.test(text)) { 116 | const argument = node.arguments[0]; 117 | if (ts.isArrayLiteralExpression(argument)) { 118 | if (argument.elements.every(elm => ts.isStringLiteral(elm) || ts.isNumericLiteral(elm))) { 119 | result = ts.createUnionTypeNode( 120 | (argument.elements as ts.NodeArray).map(elm => 121 | ts.createLiteralTypeNode(elm), 122 | ), 123 | ); 124 | } 125 | } 126 | } else if (/oneOfType$/.test(text)) { 127 | const argument = node.arguments[0]; 128 | if (ts.isArrayLiteralExpression(argument)) { 129 | result = ts.createUnionOrIntersectionTypeNode( 130 | ts.SyntaxKind.UnionType, 131 | argument.elements.map(elm => getTypeFromReactPropTypeExpression(elm)), 132 | ); 133 | } 134 | } else if (/arrayOf$/.test(text)) { 135 | const argument = node.arguments[0]; 136 | if (argument) { 137 | result = ts.createArrayTypeNode(getTypeFromReactPropTypeExpression(argument)); 138 | } 139 | } else if (/objectOf$/.test(text)) { 140 | const argument = node.arguments[0]; 141 | if (argument) { 142 | result = ts.createTypeLiteralNode([ 143 | ts.createIndexSignature( 144 | undefined, 145 | undefined, 146 | [ 147 | ts.createParameter( 148 | undefined, 149 | undefined, 150 | undefined, 151 | 'key', 152 | undefined, 153 | ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), 154 | ), 155 | ], 156 | getTypeFromReactPropTypeExpression(argument), 157 | ), 158 | ]); 159 | } 160 | } else if (/shape$/.test(text)) { 161 | const argument = node.arguments[0]; 162 | if (ts.isObjectLiteralExpression(argument)) { 163 | return buildInterfaceFromPropTypeObjectLiteral(argument); 164 | } 165 | } 166 | } 167 | 168 | /** 169 | * customProp, 170 | * anything others 171 | */ 172 | if (result === null) { 173 | result = ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); 174 | } 175 | 176 | return result; 177 | } 178 | 179 | /** 180 | * Decide if node is required 181 | * @param node React propTypes member node 182 | */ 183 | function isPropTypeRequired(node: ts.Expression) { 184 | if (!ts.isPropertyAccessExpression(node)) return false; 185 | 186 | const text = node.getText().replace(/React\.PropTypes\./, ''); 187 | return /\.isRequired/.test(text); 188 | } 189 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as _ from 'lodash'; 3 | 4 | export * from './build-prop-type-interface'; 5 | 6 | /** 7 | * If a class declaration a react class? 8 | * @param classDeclaration 9 | * @param typeChecker 10 | */ 11 | export function isReactComponent(classDeclaration: ts.ClassDeclaration, typeChecker: ts.TypeChecker): boolean { 12 | // Only classes that extend React.Component 13 | if (!classDeclaration.heritageClauses) { 14 | return false; 15 | } 16 | if (classDeclaration.heritageClauses.length !== 1) { 17 | return false; 18 | } 19 | 20 | const firstHeritageClauses = classDeclaration.heritageClauses[0]; 21 | 22 | if (firstHeritageClauses.token !== ts.SyntaxKind.ExtendsKeyword) { 23 | return false; 24 | } 25 | 26 | const expressionWithTypeArguments = firstHeritageClauses.types[0]; 27 | 28 | if (!expressionWithTypeArguments) { 29 | return false; 30 | } 31 | 32 | // Try type checker and fallback to node text 33 | const type = typeChecker.getTypeAtLocation(expressionWithTypeArguments); 34 | let typeSymbol = type && type.symbol && type.symbol.name; 35 | if (!typeSymbol) { 36 | typeSymbol = expressionWithTypeArguments.expression.getText(); 37 | } 38 | 39 | if (!/React\.Component|Component/.test(typeSymbol)) { 40 | return false; 41 | } 42 | 43 | return true; 44 | } 45 | 46 | /** 47 | * Determine if a ts.HeritageClause is React HeritageClause 48 | * 49 | * @example `extends React.Component<{}, {}>` is a React HeritageClause 50 | * 51 | * @todo: this is lazy. Use the typeChecker instead 52 | * @param clause 53 | */ 54 | export function isReactHeritageClause(clause: ts.HeritageClause) { 55 | return ( 56 | clause.token === ts.SyntaxKind.ExtendsKeyword && 57 | clause.types.length === 1 && 58 | ts.isExpressionWithTypeArguments(clause.types[0]) && 59 | /Component/.test(clause.types[0].expression.getText()) 60 | ); 61 | } 62 | 63 | /** 64 | * Return true if a statement is a React propType assignment statement 65 | * @example 66 | * SomeComponent.propTypes = { foo: React.PropTypes.string }; 67 | * @param statement 68 | */ 69 | export function isReactPropTypeAssignmentStatement(statement: ts.Statement): statement is ts.ExpressionStatement { 70 | return ( 71 | ts.isExpressionStatement(statement) && 72 | ts.isBinaryExpression(statement.expression) && 73 | statement.expression.operatorToken.kind === ts.SyntaxKind.FirstAssignment && 74 | ts.isPropertyAccessExpression(statement.expression.left) && 75 | /\.propTypes$|\.propTypes\..+$/.test(statement.expression.left.getText()) 76 | ); 77 | } 78 | 79 | /** 80 | * Does class member have a "static" member? 81 | * @param classMember 82 | */ 83 | export function hasStaticModifier(classMember: ts.ClassElement) { 84 | if (!classMember.modifiers) { 85 | return false; 86 | } 87 | const staticModifier = _.find(classMember.modifiers, modifier => { 88 | return modifier.kind == ts.SyntaxKind.StaticKeyword; 89 | }); 90 | return staticModifier !== undefined; 91 | } 92 | 93 | /** 94 | * Is class member a React "propTypes" member? 95 | * @param classMember 96 | * @param sourceFile 97 | */ 98 | export function isPropTypesMember(classMember: ts.ClassElement, sourceFile: ts.SourceFile) { 99 | try { 100 | const name = 101 | classMember.name !== undefined && ts.isIdentifier(classMember.name) ? classMember.name.escapedText : null; 102 | return name === 'propTypes'; 103 | } catch (e) { 104 | return false; 105 | } 106 | } 107 | 108 | /** 109 | * Get component name off of a propType assignment statement 110 | * @param propTypeAssignment 111 | * @param sourceFile 112 | */ 113 | export function getComponentName(propTypeAssignment: ts.Statement, sourceFile: ts.SourceFile) { 114 | const text = propTypeAssignment.getText(sourceFile); 115 | return text.substr(0, text.indexOf('.')); 116 | } 117 | 118 | /** 119 | * Convert react stateless function to arrow function 120 | * @example 121 | * Before: 122 | * function Hello(message) { 123 | * return
{message}
124 | * } 125 | * 126 | * After: 127 | * const Hello = message => { 128 | * return
{message}
129 | * } 130 | */ 131 | export function convertReactStatelessFunctionToArrowFunction( 132 | statelessFunc: ts.FunctionDeclaration | ts.VariableStatement, 133 | ) { 134 | if (ts.isVariableStatement(statelessFunc)) return statelessFunc; 135 | 136 | const funcName = statelessFunc.name || 'Component'; 137 | const funcBody = statelessFunc.body || ts.createBlock([]); 138 | 139 | const initializer = ts.createArrowFunction( 140 | undefined, 141 | undefined, 142 | statelessFunc.parameters, 143 | undefined, 144 | undefined, 145 | funcBody, 146 | ); 147 | 148 | return ts.createVariableStatement( 149 | statelessFunc.modifiers, 150 | ts.createVariableDeclarationList( 151 | [ts.createVariableDeclaration(funcName, undefined, initializer)], 152 | ts.NodeFlags.Const, 153 | ), 154 | ); 155 | } 156 | 157 | /** 158 | * Insert an item in middle of an array after a specific item 159 | * @param collection 160 | * @param afterItem 161 | * @param newItem 162 | */ 163 | export function insertAfter(collection: ArrayLike, afterItem: T, newItem: T) { 164 | const index = _.indexOf(collection, afterItem) + 1; 165 | 166 | return _.slice(collection, 0, index) 167 | .concat(newItem) 168 | .concat(_.slice(collection, index)); 169 | } 170 | 171 | /** 172 | * Insert an item in middle of an array before a specific item 173 | * @param collection 174 | * @param beforeItem 175 | * @param newItem 176 | */ 177 | export function insertBefore(collection: ArrayLike, beforeItem: T, newItems: T | T[]) { 178 | const index = _.indexOf(collection, beforeItem); 179 | 180 | return _.slice(collection, 0, index) 181 | .concat(newItems) 182 | .concat(_.slice(collection, index)); 183 | } 184 | 185 | /** 186 | * Replace an item in a collection with another item 187 | * @param collection 188 | * @param item 189 | * @param newItem 190 | */ 191 | export function replaceItem(collection: ArrayLike, item: T, newItem: T) { 192 | const index = _.indexOf(collection, item); 193 | return _.slice(collection, 0, index) 194 | .concat(newItem) 195 | .concat(_.slice(collection, index + 1)); 196 | } 197 | 198 | /** 199 | * Remove an item from a collection 200 | * @param collection 201 | * @param item 202 | * @param newItem 203 | */ 204 | export function removeItem(collection: ArrayLike, item: T) { 205 | const index = _.indexOf(collection, item); 206 | return _.slice(collection, 0, index).concat(_.slice(collection, index + 1)); 207 | } 208 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as prettier from 'prettier'; 3 | 4 | import { compile, CompilationOptions, DEFAULT_COMPILATION_OPTIONS } from './compiler'; 5 | import { reactJSMakePropsAndStateInterfaceTransformFactoryFactory } from './transforms/react-js-make-props-and-state-transform'; 6 | import { reactRemovePropTypesAssignmentTransformFactoryFactory } from './transforms/react-remove-prop-types-assignment-transform'; 7 | import { reactMovePropTypesToClassTransformFactoryFactory } from './transforms/react-move-prop-types-to-class-transform'; 8 | import { collapseIntersectionInterfacesTransformFactoryFactory } from './transforms/collapse-intersection-interfaces-transform'; 9 | import { reactRemoveStaticPropTypesMemberTransformFactoryFactory } from './transforms/react-remove-static-prop-types-member-transform'; 10 | import { reactStatelessFunctionMakePropsTransformFactoryFactory } from './transforms/react-stateless-function-make-props-transform'; 11 | import { reactRemovePropTypesImportTransformFactoryFactory } from './transforms/react-remove-prop-types-import'; 12 | 13 | export { 14 | reactMovePropTypesToClassTransformFactoryFactory, 15 | reactJSMakePropsAndStateInterfaceTransformFactoryFactory, 16 | reactStatelessFunctionMakePropsTransformFactoryFactory, 17 | collapseIntersectionInterfacesTransformFactoryFactory, 18 | reactRemovePropTypesAssignmentTransformFactoryFactory, 19 | reactRemoveStaticPropTypesMemberTransformFactoryFactory, 20 | reactRemovePropTypesImportTransformFactoryFactory, 21 | compile, 22 | }; 23 | 24 | export const allTransforms = [ 25 | reactMovePropTypesToClassTransformFactoryFactory, 26 | reactJSMakePropsAndStateInterfaceTransformFactoryFactory, 27 | reactStatelessFunctionMakePropsTransformFactoryFactory, 28 | collapseIntersectionInterfacesTransformFactoryFactory, 29 | reactRemovePropTypesAssignmentTransformFactoryFactory, 30 | reactRemoveStaticPropTypesMemberTransformFactoryFactory, 31 | reactRemovePropTypesImportTransformFactoryFactory, 32 | ]; 33 | 34 | export type TransformFactoryFactory = (typeChecker: ts.TypeChecker) => ts.TransformerFactory; 35 | 36 | /** 37 | * Run React JavaScript to TypeScript transform for file at `filePath` 38 | * @param filePath 39 | */ 40 | export function run( 41 | filePath: string, 42 | prettierOptions: prettier.Options = {}, 43 | compilationOptions: CompilationOptions = DEFAULT_COMPILATION_OPTIONS, 44 | ): string { 45 | return compile(filePath, allTransforms, prettierOptions, compilationOptions); 46 | } 47 | -------------------------------------------------------------------------------- /src/transforms/collapse-intersection-interfaces-transform.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as _ from 'lodash'; 3 | 4 | import * as helpers from '../helpers'; 5 | 6 | /** 7 | * Collapse unnecessary intersections between type literals 8 | * 9 | * @example 10 | * Before: 11 | * type Foo = {foo: string;} & {bar: number;} 12 | * 13 | * After 14 | * type Foo = {foo: string; bar: number;} 15 | */ 16 | export function collapseIntersectionInterfacesTransformFactoryFactory( 17 | typeChecker: ts.TypeChecker, 18 | ): ts.TransformerFactory { 19 | return function collapseIntersectionInterfacesTransformFactory(context: ts.TransformationContext) { 20 | return function collapseIntersectionInterfacesTransform(sourceFile: ts.SourceFile) { 21 | const visited = ts.visitEachChild(sourceFile, visitor, context); 22 | ts.addEmitHelpers(visited, context.readEmitHelpers()); 23 | 24 | return visited; 25 | 26 | function visitor(node: ts.Node) { 27 | if (ts.isTypeAliasDeclaration(node)) { 28 | return visitTypeAliasDeclaration(node); 29 | } 30 | 31 | return node; 32 | } 33 | 34 | function visitTypeAliasDeclaration(node: ts.TypeAliasDeclaration) { 35 | if (ts.isIntersectionTypeNode(node.type)) { 36 | return ts.createTypeAliasDeclaration( 37 | [], 38 | [], 39 | node.name.text, 40 | [], 41 | visitIntersectionTypeNode(node.type), 42 | ); 43 | } 44 | 45 | return node; 46 | } 47 | 48 | function visitIntersectionTypeNode(node: ts.IntersectionTypeNode) { 49 | // Only intersection of type literals can be colapsed. 50 | // We are currently ignoring intersections such as `{foo: string} & {bar: string} & TypeRef` 51 | // TODO: handle mix of type references and multiple literal types 52 | if (!node.types.every(typeNode => ts.isTypeLiteralNode(typeNode))) { 53 | return node; 54 | } 55 | 56 | // We need cast `node.type.types` to `ts.NodeArray` 57 | // because TypeScript can't figure out `node.type.types.every(ts.isTypeLiteralNode)` 58 | const types = node.types as ts.NodeArray; 59 | 60 | // Build a map of member names to all of types found in intersectioning type literals 61 | // For instance {foo: string, bar: number} & { foo: number } will result in a map like this: 62 | // Map { 63 | // 'foo' => Set { 'string', 'number' }, 64 | // 'bar' => Set { 'number' } 65 | // } 66 | const membersMap = new Map>(); 67 | 68 | // A sepecial member of type literal nodes is index signitures which don't have a name 69 | // We use this symbol to track it in our members map 70 | const INDEX_SIGNITUTRE_MEMBER = Symbol('Index signiture member'); 71 | 72 | // Keep a reference of first index signiture member parameters. (ignore rest) 73 | let indexMemberParameter: ts.NodeArray | null = null; 74 | 75 | // Iterate through all of type literal nodes members and add them to the members map 76 | types.forEach(typeNode => { 77 | typeNode.members.forEach(member => { 78 | if (ts.isIndexSignatureDeclaration(member)) { 79 | if (member.type !== undefined) { 80 | if (membersMap.has(INDEX_SIGNITUTRE_MEMBER)) { 81 | membersMap.get(INDEX_SIGNITUTRE_MEMBER)!.add(member.type); 82 | } else { 83 | indexMemberParameter = member.parameters; 84 | membersMap.set(INDEX_SIGNITUTRE_MEMBER, new Set([member.type])); 85 | } 86 | } 87 | } else if (ts.isPropertySignature(member)) { 88 | if (member.type !== undefined) { 89 | let memberName = member.name.getText(sourceFile); 90 | 91 | // For unknown reasons, member.name.getText() is returning nothing in some cases 92 | // This is probably because previous transformers did something with the AST that 93 | // index of text string of member identifier is lost 94 | // TODO: investigate 95 | if (!memberName) { 96 | memberName = (member.name as any).escapedText; 97 | } 98 | 99 | if (membersMap.has(memberName)) { 100 | membersMap.get(memberName)!.add(member.type); 101 | } else { 102 | membersMap.set(memberName, new Set([member.type])); 103 | } 104 | } 105 | } 106 | }); 107 | }); 108 | 109 | // Result type literal members list 110 | const finalMembers: Array = []; 111 | 112 | // Put together the map into a type literal that has member per each map entery and type of that 113 | // member is a union of all types in vlues for that member name in members map 114 | // if a member has only one type, create a simple type literal for it 115 | for (const [name, types] of membersMap.entries()) { 116 | if (typeof name === 'symbol') { 117 | continue; 118 | } 119 | // if for this name there is only one type found use the first type, otherwise make a union of all types 120 | let resultType = types.size === 1 ? Array.from(types)[0] : createUnionType(Array.from(types)); 121 | 122 | finalMembers.push(ts.createPropertySignature([], name, undefined, resultType, undefined)); 123 | } 124 | 125 | // Handle index signiture member 126 | if (membersMap.has(INDEX_SIGNITUTRE_MEMBER)) { 127 | const indexTypes = Array.from(membersMap.get(INDEX_SIGNITUTRE_MEMBER)!); 128 | let indexType = indexTypes[0]; 129 | if (indexTypes.length > 1) { 130 | indexType = createUnionType(indexTypes); 131 | } 132 | const indexSigniture = ts.createIndexSignature([], [], indexMemberParameter!, indexType); 133 | finalMembers.push(indexSigniture); 134 | } 135 | 136 | // Generate one single type literal node 137 | return ts.createTypeLiteralNode(finalMembers); 138 | } 139 | 140 | /** 141 | * Create a union type from multiple type nodes 142 | * @param types 143 | */ 144 | function createUnionType(types: ts.TypeNode[]) { 145 | // first dedupe literal types 146 | // TODO: this only works if all types are primitive types like string or number 147 | const uniqueTypes = _.uniqBy(types, type => type.kind); 148 | return ts.createUnionOrIntersectionTypeNode(ts.SyntaxKind.UnionType, uniqueTypes); 149 | } 150 | }; 151 | }; 152 | } 153 | -------------------------------------------------------------------------------- /src/transforms/react-js-make-props-and-state-transform.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as _ from 'lodash'; 3 | import * as helpers from '../helpers'; 4 | 5 | export type Factory = ts.TransformerFactory; 6 | 7 | /** 8 | * Get transform for transforming React code originally written in JS which does not have 9 | * props and state generic types 10 | * This transform will remove React component static "propTypes" member during transform 11 | */ 12 | export function reactJSMakePropsAndStateInterfaceTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory { 13 | return function reactJSMakePropsAndStateInterfaceTransformFactory(context: ts.TransformationContext) { 14 | return function reactJSMakePropsAndStateInterfaceTransform(sourceFile: ts.SourceFile) { 15 | const visited = visitSourceFile(sourceFile, typeChecker); 16 | ts.addEmitHelpers(visited, context.readEmitHelpers()); 17 | 18 | return visited; 19 | }; 20 | }; 21 | } 22 | 23 | function visitSourceFile(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker) { 24 | let newSourceFile = sourceFile; 25 | for (const statement of sourceFile.statements) { 26 | if (ts.isClassDeclaration(statement) && helpers.isReactComponent(statement, typeChecker)) { 27 | newSourceFile = visitReactClassDeclaration(statement, newSourceFile, typeChecker); 28 | } 29 | } 30 | 31 | return newSourceFile; 32 | } 33 | 34 | function visitReactClassDeclaration( 35 | classDeclaration: ts.ClassDeclaration, 36 | sourceFile: ts.SourceFile, 37 | typeChecker: ts.TypeChecker, 38 | ) { 39 | if (!classDeclaration.heritageClauses || !classDeclaration.heritageClauses.length) { 40 | return sourceFile; 41 | } 42 | const className = classDeclaration && classDeclaration.name && classDeclaration.name.getText(sourceFile); 43 | const propType = getPropsTypeOfReactComponentClass(classDeclaration, sourceFile); 44 | const stateType = getStateTypeOfReactComponentClass(classDeclaration, typeChecker); 45 | const shouldMakePropTypeDeclaration = propType.members.length > 0; 46 | const shouldMakeStateTypeDeclaration = !isStateTypeMemberEmpty(stateType); 47 | const propTypeName = `${className}Props`; 48 | const stateTypeName = `${className}State`; 49 | const propTypeDeclaration = ts.createTypeAliasDeclaration([], [], propTypeName, [], propType); 50 | const stateTypeDeclaration = ts.createTypeAliasDeclaration([], [], stateTypeName, [], stateType); 51 | const propTypeRef = ts.createTypeReferenceNode(propTypeName, []); 52 | const stateTypeRef = ts.createTypeReferenceNode(stateTypeName, []); 53 | 54 | const newClassDeclaration = getNewReactClassDeclaration( 55 | classDeclaration, 56 | shouldMakePropTypeDeclaration ? propTypeRef : propType, 57 | shouldMakeStateTypeDeclaration ? stateTypeRef : stateType, 58 | ); 59 | 60 | const allTypeDeclarations = []; 61 | if (shouldMakePropTypeDeclaration) allTypeDeclarations.push(propTypeDeclaration); 62 | if (shouldMakeStateTypeDeclaration) allTypeDeclarations.push(stateTypeDeclaration); 63 | 64 | let statements = helpers.insertBefore(sourceFile.statements, classDeclaration, allTypeDeclarations); 65 | statements = helpers.replaceItem(statements, classDeclaration, newClassDeclaration); 66 | return ts.updateSourceFileNode(sourceFile, statements); 67 | } 68 | 69 | function getNewReactClassDeclaration( 70 | classDeclaration: ts.ClassDeclaration, 71 | propTypeRef: ts.TypeNode, 72 | stateTypeRef: ts.TypeNode, 73 | ) { 74 | if (!classDeclaration.heritageClauses || !classDeclaration.heritageClauses.length) { 75 | return classDeclaration; 76 | } 77 | 78 | const firstHeritageClause = classDeclaration.heritageClauses[0]; 79 | 80 | const newFirstHeritageClauseTypes = helpers.replaceItem( 81 | firstHeritageClause.types, 82 | firstHeritageClause.types[0], 83 | ts.updateExpressionWithTypeArguments( 84 | firstHeritageClause.types[0], 85 | [propTypeRef, stateTypeRef], 86 | firstHeritageClause.types[0].expression, 87 | ), 88 | ); 89 | 90 | const newHeritageClauses = helpers.replaceItem( 91 | classDeclaration.heritageClauses, 92 | firstHeritageClause, 93 | ts.updateHeritageClause(firstHeritageClause, newFirstHeritageClauseTypes), 94 | ); 95 | 96 | return ts.updateClassDeclaration( 97 | classDeclaration, 98 | classDeclaration.decorators, 99 | classDeclaration.modifiers, 100 | classDeclaration.name, 101 | classDeclaration.typeParameters, 102 | newHeritageClauses, 103 | classDeclaration.members, 104 | ); 105 | } 106 | 107 | function getPropsTypeOfReactComponentClass( 108 | classDeclaration: ts.ClassDeclaration, 109 | sourceFile: ts.SourceFile, 110 | ): ts.TypeLiteralNode { 111 | const staticPropTypesMember = _.find(classDeclaration.members, member => { 112 | return ( 113 | ts.isPropertyDeclaration(member) && 114 | helpers.hasStaticModifier(member) && 115 | helpers.isPropTypesMember(member, sourceFile) 116 | ); 117 | }); 118 | 119 | if ( 120 | staticPropTypesMember !== undefined && 121 | ts.isPropertyDeclaration(staticPropTypesMember) && // check to satisfy type checker 122 | staticPropTypesMember.initializer && 123 | ts.isObjectLiteralExpression(staticPropTypesMember.initializer) 124 | ) { 125 | return helpers.buildInterfaceFromPropTypeObjectLiteral(staticPropTypesMember.initializer); 126 | } 127 | 128 | const staticPropTypesGetterMember = _.find(classDeclaration.members, member => { 129 | return ( 130 | ts.isGetAccessorDeclaration(member) && 131 | helpers.hasStaticModifier(member) && 132 | helpers.isPropTypesMember(member, sourceFile) 133 | ); 134 | }); 135 | 136 | if ( 137 | staticPropTypesGetterMember !== undefined && 138 | ts.isGetAccessorDeclaration(staticPropTypesGetterMember) // check to satisfy typechecker 139 | ) { 140 | const returnStatement = _.find(staticPropTypesGetterMember.body!.statements, statement => 141 | ts.isReturnStatement(statement), 142 | ); 143 | if ( 144 | returnStatement !== undefined && 145 | ts.isReturnStatement(returnStatement) && // check to satisfy typechecker 146 | returnStatement.expression && 147 | ts.isObjectLiteralExpression(returnStatement.expression) 148 | ) { 149 | return helpers.buildInterfaceFromPropTypeObjectLiteral(returnStatement.expression); 150 | } 151 | } 152 | 153 | return ts.createTypeLiteralNode([]); 154 | } 155 | 156 | function getStateTypeOfReactComponentClass( 157 | classDeclaration: ts.ClassDeclaration, 158 | typeChecker: ts.TypeChecker, 159 | ): ts.TypeNode { 160 | const initialState = getInitialStateFromClassDeclaration(classDeclaration, typeChecker); 161 | const initialStateIsVoid = initialState.kind === ts.SyntaxKind.VoidKeyword; 162 | const collectedStateTypes = getStateLookingForSetStateCalls(classDeclaration, typeChecker); 163 | if (!collectedStateTypes.length && initialStateIsVoid) { 164 | return ts.createTypeLiteralNode([]); 165 | } 166 | if (!initialStateIsVoid) { 167 | collectedStateTypes.push(initialState); 168 | } 169 | 170 | return ts.createUnionOrIntersectionTypeNode(ts.SyntaxKind.IntersectionType, collectedStateTypes); 171 | } 172 | 173 | /** 174 | * Get initial state of a React component looking for state value initially set 175 | * @param classDeclaration 176 | * @param typeChecker 177 | */ 178 | function getInitialStateFromClassDeclaration( 179 | classDeclaration: ts.ClassDeclaration, 180 | typeChecker: ts.TypeChecker, 181 | ): ts.TypeNode { 182 | // initial state class member 183 | 184 | const initialStateMember = _.find(classDeclaration.members, member => { 185 | try { 186 | return ts.isPropertyDeclaration(member) && member.name && member.name.getText() === 'state'; 187 | } catch (e) { 188 | return false; 189 | } 190 | }); 191 | 192 | if (initialStateMember && ts.isPropertyDeclaration(initialStateMember) && initialStateMember.initializer) { 193 | const type = typeChecker.getTypeAtLocation(initialStateMember.initializer)!; 194 | 195 | return typeChecker.typeToTypeNode(type); 196 | } 197 | 198 | // Initial state in constructor 199 | const constructor = _.find(classDeclaration.members, member => member.kind === ts.SyntaxKind.Constructor) as 200 | | ts.ConstructorDeclaration 201 | | undefined; 202 | 203 | if (constructor && constructor.body) { 204 | for (const statement of constructor.body.statements) { 205 | if ( 206 | ts.isExpressionStatement(statement) && 207 | ts.isBinaryExpression(statement.expression) && 208 | statement.expression.left.getText() === 'this.state' 209 | ) { 210 | return typeChecker.typeToTypeNode(typeChecker.getTypeAtLocation(statement.expression.right)); 211 | } 212 | } 213 | } 214 | 215 | // No initial state, fall back to void 216 | return ts.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword); 217 | } 218 | 219 | /** 220 | * Look for setState() function calls to collect the state interface in a React class component 221 | * @param classDeclaration 222 | * @param typeChecker 223 | */ 224 | function getStateLookingForSetStateCalls( 225 | classDeclaration: ts.ClassDeclaration, 226 | typeChecker: ts.TypeChecker, 227 | ): ts.TypeNode[] { 228 | const typeNodes: ts.TypeNode[] = []; 229 | for (const member of classDeclaration.members) { 230 | if (member && ts.isMethodDeclaration(member) && member.body) { 231 | lookForSetState(member.body); 232 | } 233 | } 234 | 235 | return typeNodes; 236 | 237 | function lookForSetState(node: ts.Node) { 238 | ts.forEachChild(node, lookForSetState); 239 | if ( 240 | ts.isExpressionStatement(node) && 241 | ts.isCallExpression(node.expression) && 242 | node.expression.expression.getText().match(/setState/) 243 | ) { 244 | const type = typeChecker.getTypeAtLocation(node.expression.arguments[0]); 245 | typeNodes.push(typeChecker.typeToTypeNode(type)); 246 | } 247 | } 248 | } 249 | 250 | function isStateTypeMemberEmpty(stateType: ts.TypeNode): boolean { 251 | // Only need to handle TypeLiteralNode & IntersectionTypeNode 252 | if (ts.isTypeLiteralNode(stateType)) { 253 | return stateType.members.length === 0; 254 | } 255 | 256 | if (!ts.isIntersectionTypeNode(stateType)) { 257 | return true; 258 | } 259 | 260 | return stateType.types.every(isStateTypeMemberEmpty); 261 | } 262 | -------------------------------------------------------------------------------- /src/transforms/react-move-prop-types-to-class-transform.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as _ from 'lodash'; 3 | 4 | import * as helpers from '../helpers'; 5 | 6 | export type Factory = ts.TransformerFactory; 7 | 8 | /** 9 | * Move Component.propTypes statements into class as a static member of the class 10 | * if React component is defined using a class 11 | * 12 | * Note: This transform assumes React component declaration and propTypes assignment statement 13 | * are both on root of the source file 14 | * 15 | * @example 16 | * Before: 17 | * class SomeComponent extends React.Component<{foo: number;}, {bar: string;}> {} 18 | * SomeComponent.propTypes = { foo: React.PropTypes.string } 19 | * 20 | * After 21 | * class SomeComponent extends React.Component<{foo: number;}, {bar: string;}> { 22 | * static propTypes = { foo: React.PropTypes.string } 23 | * } 24 | * 25 | * @todo 26 | * This is not supporting multiple statements for a single class yet 27 | * ``` 28 | * class SomeComponent extends React.Component<{foo: number;}, {bar: string;}> {} 29 | * SomeComponent.propTypes = { foo: React.PropTypes.string } 30 | * SomeComponent.propTypes.bar = React.PropTypes.number; 31 | * ``` 32 | */ 33 | export function reactMovePropTypesToClassTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory { 34 | return function reactMovePropTypesToClassTransformFactory(context: ts.TransformationContext) { 35 | return function reactMovePropTypesToClassTransform(sourceFile: ts.SourceFile) { 36 | const visited = visitSourceFile(sourceFile, typeChecker); 37 | ts.addEmitHelpers(visited, context.readEmitHelpers()); 38 | return visited; 39 | }; 40 | }; 41 | } 42 | 43 | /** 44 | * Make the move from propType statement to static member 45 | * @param sourceFile 46 | * @param typeChecker 47 | */ 48 | function visitSourceFile(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker) { 49 | let statements = sourceFile.statements; 50 | 51 | // Look for propType assignment statements 52 | const propTypeAssignments = statements.filter(statement => 53 | helpers.isReactPropTypeAssignmentStatement(statement), 54 | ) as ts.ExpressionStatement[]; 55 | 56 | for (const propTypeAssignment of propTypeAssignments) { 57 | // Look for the class declarations with the same name 58 | const componentName = helpers.getComponentName(propTypeAssignment, sourceFile); 59 | 60 | const classStatement = (_.find( 61 | statements, 62 | statement => 63 | ts.isClassDeclaration(statement) && 64 | statement.name !== undefined && 65 | statement.name.getText(sourceFile) === componentName, 66 | ) as {}) as ts.ClassDeclaration; // Type weirdness 67 | 68 | // && helpers.isBinaryExpression(propTypeAssignment.expression) is redundant to satisfy the type checker 69 | if (classStatement && ts.isBinaryExpression(propTypeAssignment.expression)) { 70 | const newClassStatement = addStaticMemberToClass( 71 | classStatement, 72 | 'propTypes', 73 | propTypeAssignment.expression.right, 74 | ); 75 | statements = ts.createNodeArray(helpers.replaceItem(statements, classStatement, newClassStatement)); 76 | } 77 | } 78 | 79 | return ts.updateSourceFileNode(sourceFile, statements); 80 | } 81 | 82 | /** 83 | * Insert a new static member into a class 84 | * @param classDeclaration 85 | * @param name 86 | * @param value 87 | */ 88 | function addStaticMemberToClass(classDeclaration: ts.ClassDeclaration, name: string, value: ts.Expression) { 89 | const staticModifier = ts.createToken(ts.SyntaxKind.StaticKeyword); 90 | const propertyDeclaration = ts.createProperty([], [staticModifier], name, undefined, undefined, value); 91 | return ts.updateClassDeclaration( 92 | classDeclaration, 93 | classDeclaration.decorators, 94 | classDeclaration.modifiers, 95 | classDeclaration.name, 96 | classDeclaration.typeParameters, 97 | ts.createNodeArray(classDeclaration.heritageClauses), 98 | ts.createNodeArray([propertyDeclaration, ...classDeclaration.members]), 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /src/transforms/react-remove-prop-types-assignment-transform.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import * as helpers from '../helpers'; 4 | 5 | export type Factory = ts.TransformerFactory; 6 | 7 | /** 8 | * Remove Component.propTypes statements 9 | * 10 | * @example 11 | * Before: 12 | * class SomeComponent extends React.Component<{foo: number;}, {bar: string;}> {} 13 | * SomeComponent.propTypes = { foo: React.PropTypes.string } 14 | * 15 | * After 16 | * class SomeComponent extends React.Component<{foo: number;}, {bar: string;}> {} 17 | */ 18 | export function reactRemovePropTypesAssignmentTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory { 19 | return function reactRemovePropTypesAssignmentTransformFactory(context: ts.TransformationContext) { 20 | return function reactRemovePropTypesAssignmentTransform(sourceFile: ts.SourceFile) { 21 | const visited = ts.updateSourceFileNode( 22 | sourceFile, 23 | sourceFile.statements.filter(s => !helpers.isReactPropTypeAssignmentStatement(s)), 24 | ); 25 | ts.addEmitHelpers(visited, context.readEmitHelpers()); 26 | return visited; 27 | }; 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/transforms/react-remove-prop-types-import.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as _ from 'lodash'; 3 | 4 | import * as helpers from '../helpers'; 5 | 6 | export type Factory = ts.TransformerFactory; 7 | 8 | /** 9 | * Remove `import PropTypes from 'prop-types'` or 10 | * `import { PropTypes } from 'react'` 11 | * 12 | * @example 13 | * Before: 14 | * import PropTypes from 'prop-types' 15 | * import React, { PropTypes } from 'react' 16 | * 17 | * After: 18 | * import React from 'react' 19 | */ 20 | export function reactRemovePropTypesImportTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory { 21 | return function reactRemovePropTypesImportTransformFactory(context: ts.TransformationContext) { 22 | return function reactRemovePropTypesImportTransform(sourceFile: ts.SourceFile) { 23 | const visited = ts.updateSourceFileNode( 24 | sourceFile, 25 | sourceFile.statements 26 | .filter(s => { 27 | return !( 28 | ts.isImportDeclaration(s) && 29 | ts.isStringLiteral(s.moduleSpecifier) && 30 | s.moduleSpecifier.text === 'prop-types' 31 | ); 32 | }) 33 | .map(updateReactImportIfNeeded), 34 | ); 35 | ts.addEmitHelpers(visited, context.readEmitHelpers()); 36 | return visited; 37 | }; 38 | }; 39 | } 40 | 41 | function updateReactImportIfNeeded(statement: ts.Statement) { 42 | if ( 43 | !ts.isImportDeclaration(statement) || 44 | !ts.isStringLiteral(statement.moduleSpecifier) || 45 | statement.moduleSpecifier.text !== 'react' || 46 | !statement.importClause || 47 | !statement.importClause.namedBindings || 48 | !ts.isNamedImports(statement.importClause.namedBindings) 49 | ) { 50 | return statement; 51 | } 52 | 53 | const namedBindings = statement.importClause.namedBindings; 54 | const newNamedBindingElements = namedBindings.elements.filter(elm => elm.name.text !== 'PropTypes'); 55 | 56 | if (newNamedBindingElements.length === namedBindings.elements.length) { 57 | // Means it has no 'PropTypes' named import 58 | return statement; 59 | } 60 | 61 | const newImportClause = ts.updateImportClause( 62 | statement.importClause, 63 | statement.importClause.name, 64 | newNamedBindingElements.length === 0 65 | ? undefined 66 | : ts.updateNamedImports(namedBindings, newNamedBindingElements), 67 | ); 68 | 69 | return ts.updateImportDeclaration( 70 | statement, 71 | statement.decorators, 72 | statement.modifiers, 73 | newImportClause, 74 | statement.moduleSpecifier, 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /src/transforms/react-remove-static-prop-types-member-transform.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import * as helpers from '../helpers'; 4 | 5 | export type Factory = ts.TransformerFactory; 6 | 7 | /** 8 | * Remove static propTypes 9 | * 10 | * @example 11 | * Before: 12 | * class SomeComponent extends React.Component<{foo: number;}, {bar: string;}> { 13 | * static propTypes = { 14 | * foo: React.PropTypes.number.isRequired, 15 | * } 16 | * } 17 | * 18 | * After: 19 | * class SomeComponent extends React.Component<{foo: number;}, {bar: string;}> {} 20 | */ 21 | export function reactRemoveStaticPropTypesMemberTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory { 22 | return function reactRemoveStaticPropTypesMemberTransformFactory(context: ts.TransformationContext) { 23 | return function reactRemoveStaticPropTypesMemberTransform(sourceFile: ts.SourceFile) { 24 | const visited = ts.visitEachChild(sourceFile, visitor, context); 25 | ts.addEmitHelpers(visited, context.readEmitHelpers()); 26 | return visited; 27 | 28 | function visitor(node: ts.Node) { 29 | if (ts.isClassDeclaration(node) && helpers.isReactComponent(node, typeChecker)) { 30 | return ts.updateClassDeclaration( 31 | node, 32 | node.decorators, 33 | node.modifiers, 34 | node.name, 35 | node.typeParameters, 36 | ts.createNodeArray(node.heritageClauses), 37 | node.members.filter(member => { 38 | if ( 39 | ts.isPropertyDeclaration(member) && 40 | helpers.hasStaticModifier(member) && 41 | helpers.isPropTypesMember(member, sourceFile) 42 | ) { 43 | return false; 44 | } 45 | 46 | // propTypes getter 47 | if ( 48 | ts.isGetAccessorDeclaration(member) && 49 | helpers.hasStaticModifier(member) && 50 | helpers.isPropTypesMember(member, sourceFile) 51 | ) { 52 | return false; 53 | } 54 | return true; 55 | }), 56 | ); 57 | } 58 | return node; 59 | } 60 | }; 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/transforms/react-stateless-function-make-props-transform.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as _ from 'lodash'; 3 | 4 | import * as helpers from '../helpers'; 5 | 6 | export type Factory = ts.TransformerFactory; 7 | 8 | /** 9 | * Transform react stateless components 10 | * 11 | * @example 12 | * Before: 13 | * const Hello = ({ message }) => { 14 | * return
hello {message}
15 | * } 16 | * // Or: 17 | * // const Hello = ({ message }) =>
hello {message}
18 | * 19 | * Hello.propTypes = { 20 | * message: React.PropTypes.string, 21 | * } 22 | * 23 | * After: 24 | * Type HelloProps = { 25 | * message: string; 26 | * } 27 | * 28 | * const Hello: React.SFC = ({ message }) => { 29 | * return
hello {message}
30 | * } 31 | * 32 | * Hello.propTypes = { 33 | * message: React.PropTypes.string, 34 | * } 35 | */ 36 | export function reactStatelessFunctionMakePropsTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory { 37 | return function reactStatelessFunctionMakePropsTransformFactory(context: ts.TransformationContext) { 38 | return function reactStatelessFunctionMakePropsTransform(sourceFile: ts.SourceFile) { 39 | const visited = visitSourceFile(sourceFile, typeChecker); 40 | ts.addEmitHelpers(visited, context.readEmitHelpers()); 41 | return visited; 42 | }; 43 | }; 44 | } 45 | 46 | function visitSourceFile(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker) { 47 | // Look for propType assignment statements 48 | const propTypeAssignments = sourceFile.statements.filter(statement => 49 | helpers.isReactPropTypeAssignmentStatement(statement), 50 | ) as ts.ExpressionStatement[]; 51 | 52 | let newSourceFile = sourceFile; 53 | for (const propTypeAssignment of propTypeAssignments) { 54 | const componentName = helpers.getComponentName(propTypeAssignment, newSourceFile); 55 | 56 | const funcComponent = (_.find(newSourceFile.statements, s => { 57 | return ( 58 | (ts.isFunctionDeclaration(s) && s.name !== undefined && s.name.getText() === componentName) || 59 | (ts.isVariableStatement(s) && s.declarationList.declarations[0].name.getText() === componentName) 60 | ); 61 | }) as {}) as ts.FunctionDeclaration | ts.VariableStatement; // Type weirdness 62 | 63 | if (funcComponent) { 64 | newSourceFile = visitReactStatelessComponent(funcComponent, propTypeAssignment, newSourceFile); 65 | } 66 | } 67 | 68 | return newSourceFile; 69 | } 70 | 71 | function visitReactStatelessComponent( 72 | component: ts.FunctionDeclaration | ts.VariableStatement, 73 | propTypesExpressionStatement: ts.ExpressionStatement, 74 | sourceFile: ts.SourceFile, 75 | ) { 76 | let arrowFuncComponent = helpers.convertReactStatelessFunctionToArrowFunction(component); 77 | let componentName = arrowFuncComponent.declarationList.declarations[0].name.getText(); 78 | let componentInitializer = arrowFuncComponent.declarationList.declarations[0].initializer; 79 | 80 | const propType = getPropTypesFromTypeAssignment(propTypesExpressionStatement); 81 | const shouldMakePropTypeDeclaration = propType.members.length > 0; 82 | const propTypeName = `${componentName}Props`; 83 | const propTypeDeclaration = ts.createTypeAliasDeclaration([], [], propTypeName, [], propType); 84 | const propTypeRef = ts.createTypeReferenceNode(propTypeName, []); 85 | 86 | let componentType = ts.createTypeReferenceNode(ts.createQualifiedName(ts.createIdentifier('React'), 'SFC'), [ 87 | shouldMakePropTypeDeclaration ? propTypeRef : propType, 88 | ]); 89 | 90 | // replace component with ts stateless component 91 | const typedComponent = ts.createVariableStatement( 92 | arrowFuncComponent.modifiers, 93 | ts.createVariableDeclarationList( 94 | [ts.createVariableDeclaration(componentName, componentType, componentInitializer)], 95 | arrowFuncComponent.declarationList.flags, 96 | ), 97 | ); 98 | 99 | let statements = shouldMakePropTypeDeclaration 100 | ? helpers.insertBefore(sourceFile.statements, component, [propTypeDeclaration]) 101 | : sourceFile.statements; 102 | 103 | statements = helpers.replaceItem(statements, component, typedComponent); 104 | return ts.updateSourceFileNode(sourceFile, statements); 105 | } 106 | 107 | function getPropTypesFromTypeAssignment(propTypesExpressionStatement: ts.ExpressionStatement) { 108 | if ( 109 | propTypesExpressionStatement !== undefined && 110 | ts.isBinaryExpression(propTypesExpressionStatement.expression) && 111 | ts.isObjectLiteralExpression(propTypesExpressionStatement.expression.right) 112 | ) { 113 | return helpers.buildInterfaceFromPropTypeObjectLiteral(propTypesExpressionStatement.expression.right); 114 | } 115 | 116 | return ts.createTypeLiteralNode([]); 117 | } 118 | -------------------------------------------------------------------------------- /src/untyped-modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'dedent'; 2 | -------------------------------------------------------------------------------- /test/collapse-intersection-interfaces-transform/advanced/input.tsx: -------------------------------------------------------------------------------- 1 | type Foo = { 2 | foo: string; 3 | stuff: boolean; 4 | other: () => void; 5 | } & { 6 | bar: number; 7 | [key: string]: number; 8 | }; 9 | -------------------------------------------------------------------------------- /test/collapse-intersection-interfaces-transform/advanced/output.tsx: -------------------------------------------------------------------------------- 1 | type Foo = { 2 | foo: string, 3 | stuff: boolean, 4 | other: () => void, 5 | bar: number, 6 | [key: string]: number, 7 | }; 8 | -------------------------------------------------------------------------------- /test/collapse-intersection-interfaces-transform/empty-empty/input.tsx: -------------------------------------------------------------------------------- 1 | type Foo = {} & {}; -------------------------------------------------------------------------------- /test/collapse-intersection-interfaces-transform/empty-empty/output.tsx: -------------------------------------------------------------------------------- 1 | type Foo = {}; 2 | -------------------------------------------------------------------------------- /test/collapse-intersection-interfaces-transform/multiple/input.tsx: -------------------------------------------------------------------------------- 1 | type Foo = {foo: string} & {bar: number}; 2 | 3 | type Bar = {foo: number} & {bar: string}; 4 | -------------------------------------------------------------------------------- /test/collapse-intersection-interfaces-transform/multiple/output.tsx: -------------------------------------------------------------------------------- 1 | type Foo = { 2 | foo: string, 3 | bar: number, 4 | }; 5 | type Bar = { 6 | foo: number, 7 | bar: string, 8 | }; 9 | -------------------------------------------------------------------------------- /test/collapse-intersection-interfaces-transform/repeated/input.tsx: -------------------------------------------------------------------------------- 1 | type A = { foo: string; } & { foo: string; }; 2 | 3 | type B = { foo: string; bar: number; } & { foo: number; bar: number; } 4 | 5 | type C = { foo: string; bar: number; } & { foo: number; bar: number; } & { foo: string; } 6 | -------------------------------------------------------------------------------- /test/collapse-intersection-interfaces-transform/repeated/output.tsx: -------------------------------------------------------------------------------- 1 | type A = { 2 | foo: string, 3 | }; 4 | 5 | type B = { 6 | foo: string | number, 7 | bar: number, 8 | }; 9 | 10 | type C = { 11 | foo: string | number, 12 | bar: number, 13 | }; 14 | -------------------------------------------------------------------------------- /test/collapse-intersection-interfaces-transform/simple/input.tsx: -------------------------------------------------------------------------------- 1 | type Foo = {foo: string} & {bar: number;}; -------------------------------------------------------------------------------- /test/collapse-intersection-interfaces-transform/simple/output.tsx: -------------------------------------------------------------------------------- 1 | type Foo = { 2 | foo: string, 3 | bar: number, 4 | }; 5 | -------------------------------------------------------------------------------- /test/end-to-end/basic/input.tsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import * as React from 'react'; 3 | 4 | export default class MyComponent extends React.Component { 5 | render() { 6 | return
; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/end-to-end/basic/output.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | export default class MyComponent extends React.Component<{}, {}> { 3 | render() { 4 | return
; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/end-to-end/initial-state-and-proprypes-and-set-state/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default class MyComponent extends React.Component { 4 | state = {foo: 1, bar: 'str'}; 5 | render() { 6 | return
; 7 | } 8 | otherFn() { 9 | this.setState({dynamicState: 42}); 10 | } 11 | } 12 | 13 | MyComponent.propTypes = { 14 | baz: React.PropTypes.string.isRequired, 15 | } 16 | -------------------------------------------------------------------------------- /test/end-to-end/initial-state-and-proprypes-and-set-state/output.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | type MyComponentProps = { 3 | baz: string, 4 | }; 5 | type MyComponentState = { 6 | dynamicState: number, 7 | foo: number, 8 | bar: string, 9 | }; 10 | export default class MyComponent extends React.Component { 11 | state = { foo: 1, bar: 'str' }; 12 | render() { 13 | return
; 14 | } 15 | otherFn() { 16 | this.setState({ dynamicState: 42 }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/end-to-end/initial-state-and-proprypes/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default class MyComponent extends React.Component { 4 | state = {foo: 1, bar: 'str'}; 5 | render() { 6 | return
; 7 | } 8 | } 9 | 10 | MyComponent.propTypes = { 11 | baz: React.PropTypes.string.isRequired; 12 | } 13 | -------------------------------------------------------------------------------- /test/end-to-end/initial-state-and-proprypes/output.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | type MyComponentProps = { 3 | baz: string, 4 | }; 5 | type MyComponentState = { 6 | foo: number, 7 | bar: string, 8 | }; 9 | export default class MyComponent extends React.Component { 10 | state = { foo: 1, bar: 'str' }; 11 | render() { 12 | return
; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/end-to-end/multiple-components/input.tsx: -------------------------------------------------------------------------------- 1 | const Hello = ({ message }) => { 2 | return
hello {message}
3 | }; 4 | 5 | const Hey = ({ name }) => { 6 | return
hey, {name}
7 | } 8 | 9 | Hey.propTypes = { 10 | message: React.PropTypes.string, 11 | } 12 | 13 | Hello.propTypes = { 14 | message: React.PropTypes.string, 15 | } 16 | 17 | export default class MyComponent extends React.Component { 18 | render() { 19 | return