├── .editorconfig ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __testfixtures__ ├── component-class.input.js ├── component-class.output.js ├── component-sfc.input.js ├── component-sfc.output.js ├── component.input.js ├── component.output.js ├── import-declarations-component.input.js ├── import-declarations-component.output.js ├── import-declarations-render.input.js ├── import-declarations-render.output.js ├── import-declarations.input.js ├── import-declarations.output.js ├── props.input.js ├── props.output.js ├── removePropTypes.input.js ├── removePropTypes.output.js ├── render.input.js ├── render.output.js ├── state.input.js ├── state.output.js ├── transform-nested.input.js └── transform-nested.output.js ├── __tests__ └── transform-test.js ├── package-lock.json ├── package.json ├── transform.js └── transforms ├── component-class.js ├── component-sfc.js ├── component.js ├── import-declarations.js ├── props.js ├── removePropTypes.js ├── render.js └── state.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | coverage/ 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json 3 | yarn.lock -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '6' 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `preact-codemod` 2 | 3 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) and [clone](https://help.github.com/articles/cloning-a-repository/) this repository. 4 | 5 | ```bash 6 | $ git clone git@github.com:vutran/preact-codemod.git 7 | ``` 8 | 9 | 2. Switch to the new `preact-codemod` directory that was created in step 1. 10 | 11 | ```bash 12 | $ cd preact-codemod 13 | ``` 14 | 15 | 3. Install `npm` dependencies. 16 | 17 | ```bash 18 | $ npm install 19 | ``` 20 | 21 | 4. Create a transform in the `./transforms` directory. 22 | 5. Create a test fixture in the `./__testfixtures__` directory. 23 | 6. Create a test in the `./__tests__` directory. 24 | 7. Submit a [Pull Request](https://github.com/vutran/preact-codemod/pulls) if you have something great to contribute and a review will proceed. 25 | 26 | ## Bug Reports 27 | 28 | Found a bug? Submit it to the [issue tracker](https://github.com/vutran/preact-codemod/issues) and/or send a [Pull Request](#pull-requests) so we can get it squashed. 29 | 30 | ## Feature Requests 31 | 32 | If you have an idea for a feature, please post them to the [issues tracker](https://github.com/vutran/preact-codemod/issues) and we can discuss. Not all features will be implemented. 33 | 34 | ## Pull Requests 35 | 36 | Make sure your transforms are accompanied by a test fixture and a test. Submit all Pull Requests to the master branch. If you have an idea and don't know if it will get accepted, please file an issue and we can discuss whether it could be merged into core. 37 | 38 | ## Feedback and Suggestions 39 | 40 | All feedback and suggestions for `preact-codemod` are welcomed. If you have one, please send a tweet to [@tranvu](https://twitter.com/tranvu/) and we can discuss. 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Vu Tran 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # preact-codemod 2 | 3 | [![Build Status](https://travis-ci.org/vutran/preact-codemod.svg?branch=master)](https://travis-ci.org/vutran/preact-codemod) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/vutran/preact-codemod/blob/master/LICENSE) 4 | 5 | > Transform your React code to Preact. 6 | 7 | ## Demo: [astexplorer](http://astexplorer.net/#/Pcp2Q5NFLq/10) 8 | 9 | ## Tests 10 | 11 | ```bash 12 | $ npm test 13 | ``` 14 | 15 | ## Goals 16 | 17 | * Learning and experimenting with jscodeshift by transforming React to Preact. 18 | * Eventually can convert a big React application. 19 | 20 | ## Contributing 21 | 22 | ♥ **_`preact-codemod`_** and want to contribute? You can help contribute to the growth of this codemod in many ways. 23 | 24 | * [Bug Reports](CONTRIBUTING.md#bug-reports) 25 | * [Feature Requests](CONTRIBUTING.md#feature-requests) 26 | * [Pull Requests](CONTRIBUTING.md#pull-requests) 27 | * [Feedback and Suggestions](CONTRIBUTING.md#feedback-and-suggestions) 28 | 29 | ## Support 30 | 31 | Like what you see? [Become a Patron](https://www.patreon.com/vutran) and support me via a monthly donation. 32 | 33 | ## License 34 | 35 | MIT © [Vu Tran](https://github.com/vutran/) 36 | -------------------------------------------------------------------------------- /__testfixtures__/component-class.input.js: -------------------------------------------------------------------------------- 1 | const App = React.createClass({ 2 | foo: 'bar', 3 | shouldComponentUpdate: function() { 4 | return true; 5 | }, 6 | render: function() { 7 | return
Test
; 8 | }, 9 | }); 10 | 11 | const App1 = React.createClass({ id: 1 }), 12 | App2 = React.createClass({ id: 2 }); 13 | -------------------------------------------------------------------------------- /__testfixtures__/component-class.output.js: -------------------------------------------------------------------------------- 1 | class App extends Component { 2 | constructor(props, context) { 3 | super(props, context); 4 | this.foo = 'bar'; 5 | } 6 | 7 | shouldComponentUpdate() { 8 | return true; 9 | } 10 | 11 | render() { 12 | return
Test
; 13 | } 14 | } 15 | 16 | class App1 extends Component { 17 | constructor(props, context) { 18 | super(props, context); 19 | this.id = 1; 20 | } 21 | } 22 | 23 | class App2 extends Component { 24 | constructor(props, context) { 25 | super(props, context); 26 | this.id = 2; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /__testfixtures__/component-sfc.input.js: -------------------------------------------------------------------------------- 1 | const App = React.createClass({ 2 | render: function() { 3 | return ( 4 |
5 | Hello, World! 6 | I get transformed to Preact! 7 |
8 | ); 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /__testfixtures__/component-sfc.output.js: -------------------------------------------------------------------------------- 1 | const App = function() { 2 | return ( 3 |
4 | Hello, World! 5 | I get transformed to Preact! 6 |
7 | ); 8 | }; 9 | -------------------------------------------------------------------------------- /__testfixtures__/component.input.js: -------------------------------------------------------------------------------- 1 | const PureComponent = React.createClass({ 2 | render: function() { 3 | return
PureComponent Test
; 4 | }, 5 | }); 6 | 7 | const ImpureComponent = React.createClass({ 8 | shouldComponentUpdate: function() { 9 | return true; 10 | }, 11 | render: function() { 12 | return
ImpureComponent Test
; 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /__testfixtures__/component.output.js: -------------------------------------------------------------------------------- 1 | const PureComponent = function() { 2 | return
PureComponent Test
; 3 | }; 4 | 5 | class ImpureComponent extends Component { 6 | shouldComponentUpdate() { 7 | return true; 8 | } 9 | 10 | render() { 11 | return
ImpureComponent Test
; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /__testfixtures__/import-declarations-component.input.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const ReactDOM = require('react-dom'); 3 | 4 | const Foo = React.createClass({ 5 | render: function() { 6 | return
Bar
; 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /__testfixtures__/import-declarations-component.output.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | 3 | const Foo = React.createClass({ 4 | render: function() { 5 | return
Bar
; 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /__testfixtures__/import-declarations-render.input.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const ReactDOM = require('react-dom'); 3 | 4 | ReactDOM.render(
Foo
, document.body); 5 | -------------------------------------------------------------------------------- /__testfixtures__/import-declarations-render.output.js: -------------------------------------------------------------------------------- 1 | import { h, render } from 'preact'; 2 | 3 | ReactDOM.render(
Foo
, document.body); 4 | -------------------------------------------------------------------------------- /__testfixtures__/import-declarations.input.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const ReactDOM = require('react-dom'); 3 | -------------------------------------------------------------------------------- /__testfixtures__/import-declarations.output.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | -------------------------------------------------------------------------------- /__testfixtures__/props.input.js: -------------------------------------------------------------------------------- 1 | const TodoItem = React.createClass({ 2 | render: function() { 3 | return
{this.props.item}
; 4 | }, 5 | }); 6 | 7 | const TodoItem = function() { 8 | return
{this.props.item}
; 9 | }; 10 | 11 | class TodoItem extends Component { 12 | render() { 13 | return
{this.props.item}
; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /__testfixtures__/props.output.js: -------------------------------------------------------------------------------- 1 | const TodoItem = React.createClass({ 2 | render: function(props) { 3 | return
{props.item}
; 4 | }, 5 | }); 6 | 7 | const TodoItem = function(props) { 8 | return
{props.item}
; 9 | }; 10 | 11 | class TodoItem extends Component { 12 | render(props) { 13 | return
{props.item}
; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /__testfixtures__/removePropTypes.input.js: -------------------------------------------------------------------------------- 1 | import { PropTypes } from 'react'; 2 | 3 | TodoItem.propTypes = { 4 | foo: PropTypes.string, 5 | }; 6 | -------------------------------------------------------------------------------- /__testfixtures__/removePropTypes.output.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vutran/preact-codemod/5e24254d61507fc7c75dc0f52e301b3f49b551b2/__testfixtures__/removePropTypes.output.js -------------------------------------------------------------------------------- /__testfixtures__/render.input.js: -------------------------------------------------------------------------------- 1 | ReactDOM.render(, document.body); 2 | -------------------------------------------------------------------------------- /__testfixtures__/render.output.js: -------------------------------------------------------------------------------- 1 | render(, document.body); 2 | -------------------------------------------------------------------------------- /__testfixtures__/state.input.js: -------------------------------------------------------------------------------- 1 | const TodoItem = React.createClass({ 2 | render: function() { 3 | return
{this.state.item}
; 4 | }, 5 | }); 6 | 7 | const TodoItem = function() { 8 | return
{this.state.item}
; 9 | }; 10 | 11 | class TodoItem extends Component { 12 | render() { 13 | return
{this.state.item}
; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /__testfixtures__/state.output.js: -------------------------------------------------------------------------------- 1 | const TodoItem = React.createClass({ 2 | render: function(props, state) { 3 | return
{state.item}
; 4 | }, 5 | }); 6 | 7 | const TodoItem = function(props, state) { 8 | return
{state.item}
; 9 | }; 10 | 11 | class TodoItem extends Component { 12 | render(props, state) { 13 | return
{state.item}
; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /__testfixtures__/transform-nested.input.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const ReactDOM = require('react-dom'); 3 | 4 | const data = { 5 | items: [ 6 | { name: 'Buy milk', done: false }, 7 | { name: 'Do laundry', done: false }, 8 | ], 9 | }; 10 | 11 | const TodoItem = React.createClass({ 12 | render: function() { 13 | return ( 14 |
15 | 16 | {this.props.item.name} 17 |
18 | ); 19 | }, 20 | }); 21 | 22 | const App = React.createClass({ 23 | render: function() { 24 | return ( 25 |
{this.props.data.items.map(item => )}
26 | ); 27 | }, 28 | }); 29 | 30 | ReactDOM.render(, document.body); 31 | -------------------------------------------------------------------------------- /__testfixtures__/transform-nested.output.js: -------------------------------------------------------------------------------- 1 | import { h, Component, render } from 'preact'; 2 | 3 | const data = { 4 | items: [ 5 | { name: 'Buy milk', done: false }, 6 | { name: 'Do laundry', done: false }, 7 | ], 8 | }; 9 | 10 | const TodoItem = function(props, state) { 11 | return ( 12 |
13 | 14 | {props.item.name} 15 |
16 | ); 17 | }; 18 | 19 | const App = function(props) { 20 | return
{props.data.items.map(item => )}
; 21 | }; 22 | 23 | render(, document.body); 24 | -------------------------------------------------------------------------------- /__tests__/transform-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | const path = require('path'); 4 | const defineTest = require('jscodeshift/dist/testUtils').defineTest; 5 | 6 | defineTest(__dirname, 'transforms/component', null, 'component'); 7 | defineTest(__dirname, 'transforms/component-sfc', null, 'component-sfc'); 8 | defineTest(__dirname, 'transforms/component-class', null, 'component-class'); 9 | defineTest( 10 | __dirname, 11 | 'transforms/import-declarations', 12 | null, 13 | 'import-declarations-component' 14 | ); 15 | defineTest( 16 | __dirname, 17 | 'transforms/import-declarations', 18 | null, 19 | 'import-declarations-render' 20 | ); 21 | defineTest( 22 | __dirname, 23 | 'transforms/import-declarations', 24 | null, 25 | 'import-declarations' 26 | ); 27 | defineTest(__dirname, 'transforms/props', null, 'props'); 28 | defineTest(__dirname, 'transforms/removePropTypes', null, 'removePropTypes'); 29 | defineTest(__dirname, 'transforms/render', null, 'render'); 30 | defineTest(__dirname, 'transforms/state', null, 'state'); 31 | 32 | // global tests 33 | defineTest(__dirname, 'transform', null, 'transform-nested'); 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-codemod", 3 | "version": "0.1.1", 4 | "description": "Transform your React code to Preact", 5 | "main": "transform.js", 6 | "scripts": { 7 | "fmt": "npm run fmt:js && npm run fmt:md", 8 | "fmt:js": "prettier --write '{*.js,*.json,{__testfixtures__,__tests__,transforms}/**/*.js{,x}}'", 9 | "fmt:md": "prettier --parser markdown --write '*.md'", 10 | "precommit": "lint-staged", 11 | "test": "jest" 12 | }, 13 | "author": "Vu Tran ", 14 | "license": "MIT", 15 | "repository": { 16 | "type": "git", 17 | "url": "git@github.com:vutran/preact-codemod.git" 18 | }, 19 | "homepage": "https://github.com/vutran/preact-codemod", 20 | "bugs": { 21 | "web": "https://github.com/vutran/preact-codemod/issues" 22 | }, 23 | "devDependencies": { 24 | "husky": "^0.14.3", 25 | "jest": "^22.4.3", 26 | "lint-staged": "^6.1.0", 27 | "prettier": "^1.10.2" 28 | }, 29 | "dependencies": { 30 | "jscodeshift": "^0.4.0" 31 | }, 32 | "lint-staged": { 33 | "{*.js,{src,__tests__,typings}/**/*.ts{,x}}": [ 34 | "prettier --write", 35 | "git add" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /transform.js: -------------------------------------------------------------------------------- 1 | const importDeclarations = require('./transforms/import-declarations'); 2 | const component = require('./transforms/component'); 3 | const render = require('./transforms/render'); 4 | const props = require('./transforms/props'); 5 | const state = require('./transforms/state'); 6 | 7 | /** 8 | * Magic 9 | */ 10 | module.exports = (file, api) => { 11 | let source = file.source; 12 | 13 | source = importDeclarations({ source }, api); 14 | source = component({ source }, api); 15 | source = render({ source }, api); 16 | source = props({ source }, api); 17 | source = state({ source }, api); 18 | 19 | return source; 20 | }; 21 | -------------------------------------------------------------------------------- /transforms/component-class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Transforms React.createClass to an ES2015 class 3 | */ 4 | module.exports = (file, api) => { 5 | const j = api.jscodeshift; 6 | const root = j(file.source); 7 | 8 | root 9 | .find(j.Identifier, n => n.name === 'createClass') 10 | .closest(j.VariableDeclaration) 11 | .replaceWith(v => { 12 | return v.value.declarations.map(n => { 13 | const className = n.id.name; 14 | const props = n.init.arguments[0].properties; 15 | const literals = []; 16 | const methods = []; 17 | const body = []; 18 | const constructorBody = [ 19 | { 20 | type: 'ExpressionStatement', 21 | expression: { 22 | type: 'CallExpression', 23 | callee: { 24 | type: 'Super', 25 | }, 26 | arguments: [ 27 | { 28 | type: 'Identifier', 29 | name: 'props', 30 | }, 31 | { 32 | type: 'Identifier', 33 | name: 'context', 34 | }, 35 | ], 36 | }, 37 | }, 38 | ]; 39 | // iterate through each props 40 | // and build a definition for all class 41 | // properties and methods 42 | props.forEach(prop => { 43 | switch (prop.value.type) { 44 | case 'FunctionExpression': 45 | methods.push({ 46 | type: 'MethodDefinition', 47 | key: prop.key, 48 | value: prop.value, 49 | }); 50 | break; 51 | case 'Literal': 52 | literals.push({ 53 | type: 'ExpressionStatement', 54 | expression: { 55 | type: 'AssignmentExpression', 56 | operator: '=', 57 | left: { 58 | type: 'MemberExpression', 59 | object: { 60 | type: 'ThisExpression', 61 | }, 62 | property: { 63 | type: 'Identifier', 64 | name: prop.key.name, 65 | }, 66 | }, 67 | right: { 68 | type: 'Literal', 69 | value: prop.value.value, 70 | }, 71 | }, 72 | }); 73 | break; 74 | } 75 | }); 76 | // build the body 77 | if (literals.length) { 78 | body.unshift({ 79 | type: 'MethodDefinition', 80 | key: { 81 | type: 'Identifier', 82 | name: 'constructor', 83 | }, 84 | static: false, 85 | kind: 'constructor', 86 | value: { 87 | type: 'FunctionExpression', 88 | params: [ 89 | { 90 | type: 'Identifier', 91 | name: 'props', 92 | }, 93 | { 94 | type: 'Identifier', 95 | name: 'context', 96 | }, 97 | ], 98 | body: { 99 | type: 'BlockStatement', 100 | body: constructorBody.concat(literals), 101 | }, 102 | }, 103 | }); 104 | } 105 | // create the class 106 | return { 107 | type: 'ClassDeclaration', 108 | id: { 109 | type: 'Identifier', 110 | name: n.id.name, 111 | }, 112 | superClass: { 113 | type: 'Identifier', 114 | name: 'Component', 115 | }, 116 | body: { 117 | type: 'ClassBody', 118 | body: body.concat(methods), 119 | }, 120 | }; 121 | }); 122 | }); 123 | 124 | return root.toSource({ quote: 'single' }); 125 | }; 126 | -------------------------------------------------------------------------------- /transforms/component-sfc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Transforms React.createClass to a pure function 3 | */ 4 | module.exports = (file, api) => { 5 | const j = api.jscodeshift; 6 | const root = j(file.source); 7 | 8 | root 9 | .find(j.Identifier, n => n.name === 'createClass') 10 | .closest(j.CallExpression) 11 | .find(j.JSXElement) 12 | .filter(n => n.parentPath.value.type === 'ReturnStatement') 13 | .closest(j.CallExpression) 14 | .replaceWith( 15 | n => 16 | n.value.arguments[0].properties.filter(p => p.key.name === 'render')[0] 17 | .value 18 | ); 19 | 20 | return root.toSource({ quote: 'single' }); 21 | }; 22 | -------------------------------------------------------------------------------- /transforms/component.js: -------------------------------------------------------------------------------- 1 | const sfc = require('./component-sfc'); 2 | const cls = require('./component-class'); 3 | 4 | /** 5 | * Conditionally transforms a component: 6 | * - class component 7 | * - stateless functional component 8 | */ 9 | module.exports = (file, api) => { 10 | const j = api.jscodeshift; 11 | const root = j(file.source); 12 | 13 | const pureProps = ['render', 'defaultProps', 'displayName']; 14 | 15 | root 16 | .find(j.Identifier, n => n.name === 'createClass') 17 | .closest(j.VariableDeclaration) 18 | .replaceWith(v => { 19 | return v.value.declarations.map(n => { 20 | const props = n.init.arguments[0].properties; 21 | const propKeys = props.map(p => p.key.name); 22 | if (propKeys.filter(p => pureProps.indexOf(p) === -1).length) { 23 | // transform to ES2015 classes 24 | const nv = { 25 | type: v.value.type, 26 | kind: v.value.kind, 27 | declarations: [j(n).toSource()], 28 | }; 29 | return cls({ source: j(nv).toSource() }, api); 30 | } else { 31 | // transform to pure function 32 | const nv = { 33 | type: v.value.type, 34 | kind: v.value.kind, 35 | declarations: [j(n).toSource()], 36 | }; 37 | return sfc({ source: j(nv).toSource() }, api); 38 | } 39 | }); 40 | }); 41 | 42 | return root.toSource({ quote: 'single' }); 43 | }; 44 | -------------------------------------------------------------------------------- /transforms/import-declarations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates and returns an import declaration 3 | * 4 | * @param {String} source - The source module 5 | * @param {String[]} values - The named values to import 6 | * @return {Node} 7 | */ 8 | const importDeclaration = (source, values) => { 9 | const node = { 10 | type: 'ImportDeclaration', 11 | importKind: 'value', 12 | source: { type: 'Literal', value: source }, 13 | specifiers: [], 14 | }; 15 | values.forEach(v => { 16 | node.specifiers.push({ 17 | type: 'ImportSpecifier', 18 | imported: { type: 'Identifier', name: v }, 19 | local: { type: 'Identifier', name: v }, 20 | }); 21 | }); 22 | return node; 23 | }; 24 | 25 | /** 26 | * Transforms React imports to Preact imports 27 | */ 28 | module.exports = (file, api) => { 29 | const j = api.jscodeshift; 30 | const root = j(file.source); 31 | const namedImports = new Set(['h']); 32 | 33 | // should import preact.Component? 34 | root 35 | .find(j.MemberExpression) 36 | .filter( 37 | n => 38 | n.value.object.name === 'React' && 39 | n.value.property.name === 'createClass' 40 | ) 41 | .forEach(n => { 42 | namedImports.add('Component'); 43 | }); 44 | 45 | // should import preact.render? 46 | root 47 | .find(j.MemberExpression) 48 | .filter( 49 | n => 50 | n.value.object.name === 'ReactDOM' && n.value.property.name === 'render' 51 | ) 52 | .forEach(n => { 53 | namedImports.add('render'); 54 | }); 55 | 56 | // remove react 57 | root 58 | .findVariableDeclarators('React') 59 | .closest(j.VariableDeclaration) 60 | .remove(); 61 | 62 | // replace render 63 | root 64 | .findVariableDeclarators('ReactDOM') 65 | .closest(j.VariableDeclaration) 66 | .replaceWith(importDeclaration('preact', namedImports)); 67 | 68 | return root.toSource({ quote: 'single' }); 69 | }; 70 | -------------------------------------------------------------------------------- /transforms/props.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Makes props available as the argument in its calling function. 3 | * Also strips the `this` context in the JSX expression. 4 | */ 5 | module.exports = (file, api) => { 6 | const j = api.jscodeshift; 7 | const root = j(file.source); 8 | 9 | // strip this context and add the props to the function params 10 | root 11 | .find(j.ThisExpression) 12 | .closest(j.MemberExpression) 13 | .filter( 14 | n => 15 | n.value.object.type === 'ThisExpression' && 16 | n.value.property.name === 'props' 17 | ) 18 | .closest(j.MemberExpression) 19 | .replaceWith(n => { 20 | const prop = n.value.property.name; 21 | const newNode = { 22 | type: 'MemberExpression', 23 | object: { 24 | type: 'Identifier', 25 | name: 'props', 26 | }, 27 | property: { 28 | type: 'Identifier', 29 | name: prop, 30 | }, 31 | }; 32 | if (prop) { 33 | j(n) 34 | .closest(j.FunctionExpression) 35 | .forEach(n1 => { 36 | if (!n1.value.params.length) { 37 | n1.value.params.push('props'); 38 | } 39 | }); 40 | } 41 | return newNode; 42 | }); 43 | 44 | return root.toSource({ quote: 'single' }); 45 | }; 46 | -------------------------------------------------------------------------------- /transforms/removePropTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Removes the named import: PropTypes 3 | */ 4 | const removeNamedImportPropTypes = (file, api) => { 5 | const j = api.jscodeshift; 6 | const root = j(file.source); 7 | root 8 | .find(j.ImportSpecifier) 9 | .filter(n => n.value.imported.name === 'PropTypes') 10 | .remove(); 11 | 12 | return root.toSource({ quote: 'single' }); 13 | }; 14 | 15 | /** 16 | * Removes unnecessary empty import declarations 17 | */ 18 | const removeEmptyImports = (file, api) => { 19 | const j = api.jscodeshift; 20 | const root = j(file.source); 21 | root 22 | .find(j.ImportDeclaration) 23 | .filter(n => !n.value.specifiers.length) 24 | .remove(); 25 | 26 | return root.toSource({ quote: 'single' }); 27 | }; 28 | 29 | /** 30 | * Removes all propTypes assignments 31 | */ 32 | const removePropTypesAssignments = (file, api) => { 33 | const j = api.jscodeshift; 34 | const root = j(file.source); 35 | root 36 | .find(j.AssignmentExpression) 37 | .filter(n => n.value.left.property.name === 'propTypes') 38 | .remove(); 39 | 40 | return root.toSource({ quote: 'single' }); 41 | }; 42 | 43 | module.exports = (file, api) => { 44 | let source = file.source; 45 | 46 | source = removeNamedImportPropTypes({ source }, api); 47 | source = removeEmptyImports({ source }, api); 48 | source = removePropTypesAssignments({ source }, api); 49 | 50 | return source; 51 | }; 52 | -------------------------------------------------------------------------------- /transforms/render.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Transforms ReactDOM.render to preact.render 3 | */ 4 | module.exports = (file, api) => { 5 | const j = api.jscodeshift; 6 | const root = j(file.source); 7 | 8 | root 9 | .find(j.MemberExpression) 10 | .filter( 11 | n => 12 | n.value.object.name === 'ReactDOM' && n.value.property.name === 'render' 13 | ) 14 | .replaceWith({ type: 'Identifier', name: 'render' }); 15 | 16 | return root.toSource({ quote: 'single' }); 17 | }; 18 | -------------------------------------------------------------------------------- /transforms/state.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Makes state available as the argument in its calling function. 3 | * Also strips the `this` context in the JSX expression. 4 | */ 5 | module.exports = (file, api) => { 6 | const j = api.jscodeshift; 7 | const root = j(file.source); 8 | 9 | // strip this context and add the props to the function params 10 | root 11 | .find(j.ThisExpression) 12 | .closest(j.MemberExpression) 13 | .filter( 14 | n => 15 | n.value.object.type === 'ThisExpression' && 16 | n.value.property.name === 'state' 17 | ) 18 | .closest(j.MemberExpression) 19 | .replaceWith(n => { 20 | const prop = n.value.property.name; 21 | const newNode = { 22 | type: 'MemberExpression', 23 | object: { 24 | type: 'Identifier', 25 | name: 'state', 26 | }, 27 | property: { 28 | type: 'Identifier', 29 | name: prop, 30 | }, 31 | }; 32 | if (prop) { 33 | j(n) 34 | .closest(j.FunctionExpression) 35 | .forEach(n1 => { 36 | if (!n1.value.params.length) { 37 | n1.value.params.push('props'); 38 | } 39 | n1.value.params.push('state'); 40 | }); 41 | } 42 | return newNode; 43 | }); 44 | 45 | return root.toSource({ quote: 'single' }); 46 | }; 47 | --------------------------------------------------------------------------------