├── .babelrc ├── .eslintignore ├── .eslintrc ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── LICENSE ├── README.md ├── bin └── vtr ├── commitlint.config.js ├── demo ├── react.js ├── sfc.js ├── sfc.vue └── vue.js ├── package.json └── src ├── collect-state.js ├── index.js ├── output.js ├── react-ast-helpers.js ├── sfc ├── directives.js ├── event-map.js ├── index.js └── sfc-ast-helpers.js ├── utils.js ├── vue-ast-helpers.js ├── vue-computed.js └── vue-props.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["env", { 3 | "targets": { 4 | "node": "current" 5 | }, 6 | "modules": "commonjs", 7 | "debug": true 8 | }]] 9 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | demo/**/*.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true 5 | }, 6 | "extends": "standard", 7 | "rules": { 8 | "indent": [2, 4, { "SwitchCase": 1 }], 9 | "quotes": [2, "single", { "allowTemplateLiterals": true }], 10 | "linebreak-style": [2, "unix"], 11 | "semi": [2, "always"], 12 | "eqeqeq": [2, "always"], 13 | "strict": [2, "global"], 14 | "key-spacing": [2, { "afterColon": true }], 15 | "no-console": 0, 16 | "no-debugger": 0, 17 | "no-empty": 0, 18 | "no-unused-vars": 0, 19 | "no-constant-condition": 0, 20 | "no-undef": 0, 21 | "no-trailing-spaces": 0, 22 | "no-unneeded-ternary": 0 23 | } 24 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Your Problem 2 | The description for your problem... 3 | ## Your Code 4 | ``` 5 | // your code is here 6 | ``` 7 | ## Your Command 8 | ``` 9 | // your command is here 10 | ``` 11 | ## Error Info 12 | ``` 13 | // error info 14 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | 41 | .idea 42 | .DS_Store 43 | node_modules 44 | package-lock.json 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pomy 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![npm-version](https://img.shields.io/npm/v/vue-to-react.svg) ![license](https://img.shields.io/github/license/dwqs/vue-to-react.svg) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com) 2 | 3 | ## vue-to-react 4 | 🛠️ 👉 Try to transform Vue component(support [JSX](https://github.com/vuejs/babel-plugin-transform-vue-jsx) and [SFC](https://vuejs.org/v2/guide/single-file-components.html)) to React component. 5 | > Since v0.0.8 support SFC 6 | 7 | ## Preview screenshots 8 | **Transform JSX Component:** 9 | 10 | ![jsx](https://user-images.githubusercontent.com/7871813/40406386-0bfc0396-5e93-11e8-9f74-7a45d2694ae9.png) 11 | 12 | **Transform SFC Component:** 13 | 14 | ![sfc](https://user-images.githubusercontent.com/7871813/40526210-9afc8112-6017-11e8-8266-c0b7920281e2.png) 15 | 16 | ### Install 17 | Prerequisites: [Node.js](https://nodejs.org/en/) (>=8.0) and [NPM](https://www.npmjs.com/) (>=5.0) 18 | 19 | ```js 20 | $ npm install vue-to-react -g 21 | ``` 22 | 23 | ### Usage 24 | ```sh 25 | Usage: vtr [options] 26 | 27 | Options: 28 | 29 | -V, --version output the version number 30 | -i, --input the input path for vue component 31 | -o, --output the output path for react component, which default value is process.cwd() 32 | -n, --name the output file name, which default value is "react.js" 33 | -h, --help output usage information 34 | 35 | ``` 36 | 37 | Examples: 38 | 39 | ```sh 40 | $ vtr -i my/vue/component 41 | ``` 42 | 43 | The above code will transform `my/vue/component.js` to `${process.cwd()}/react.js`. 44 | 45 | ```sh 46 | $ vtr -i my/vue/component -o my/vue -n test 47 | ``` 48 | 49 | The above code will transform `my/vue/component.js` to `my/vue/test.js`. 50 | 51 | Here is a [demo](https://github.com/dwqs/vue-to-react/tree/master/demo). 52 | 53 | ## Attention 54 | The following list you should be pay attention when you are using vue-to-react to transform a vue component to react component: 55 | 56 | * Not support [class object syntax binding](https://vuejs.org/v2/guide/class-and-style.html#Object-Syntax) and [class array syntax binding](https://vuejs.org/v2/guide/class-and-style.html#Array-Syntax) 57 | 58 | ```js 59 | // Not support 60 |
61 |
62 | 63 | // support 64 |
65 | computed: { 66 | classes () { 67 | // ... 68 | return your-classes; 69 | } 70 | } 71 | 72 | // ... 73 | 74 | // react component 75 | // ... 76 | 77 | render () { 78 | const classes = your-classes; 79 | return ( 80 |
81 | ) 82 | } 83 | 84 | ``` 85 | 86 | * Not support [style object syntax binding](https://vuejs.org/v2/guide/class-and-style.html#Object-Syntax-1) and [style array syntax binding](https://vuejs.org/v2/guide/class-and-style.html#Array-Syntax-1) 87 | 88 | ```js 89 | // Not support 90 |
91 |
92 | 93 | // support 94 |
95 | computed: { 96 | style () { 97 | return { 98 | activeColor: 'red', 99 | fontSize: 30 100 | } 101 | } 102 | } 103 | 104 | // ... 105 | 106 | // react component 107 | // ... 108 | 109 | render () { 110 | const style = { 111 | activeColor: 'red', 112 | fontSize: 30 113 | }; 114 | return ( 115 |
116 | ) 117 | } 118 | 119 | ``` 120 | 121 | * Not support `watch` prop of vue component 122 | * Not support `components` prop of vue component if you are transforming a JSX component. See [component tip](https://github.com/vuejs/babel-plugin-transform-vue-jsx#component-tip). But support `components` prop when you are transforming SFC. 123 | * Only supports partial built-in Vue directives(SFC): `v-if`, `v-else`, `v-show`, `v-for`, `v-bind`, `v-on`, `v-text` and `v-html`. 124 | * Not support v-bind shorthand and v-on shorthand(SFC): 125 | 126 | ```js 127 | // Not support 128 |
129 | 130 | // Support 131 |
132 | ``` 133 | 134 | * Not support custom directives and filter expression(SFC). 135 | * Only supports partial lift-cycle methods of vue component. Lift-cycle relations mapping as follows: 136 | 137 | ```js 138 | // Life-cycle methods relations mapping 139 | const cycle = { 140 | 'created': 'componentWillMount', 141 | 'mounted': 'componentDidMount', 142 | 'updated': 'componentDidUpdate', 143 | 'beforeDestroy': 'componentWillUnmount', 144 | 'errorCaptured': 'componentDidCatch', 145 | 'render': 'render' 146 | }; 147 | ``` 148 | 149 | * Each computed prop should be a function: 150 | 151 | ```js 152 | // ... 153 | 154 | computed: { 155 | // support 156 | test () { 157 | return your-computed-value; 158 | }, 159 | 160 | // not support 161 | test2: { 162 | get () {}, 163 | set () {} 164 | } 165 | } 166 | 167 | // ... 168 | ``` 169 | 170 | * Computed prop of vue component will be put into the render method of react component: 171 | 172 | ```js 173 | // vue component 174 | // ... 175 | 176 | computed: { 177 | // support 178 | test () { 179 | this.title = 'messages'; // Don't do this, it won't be handle and you will receive a warning. 180 | return this.title + this.msg; 181 | } 182 | } 183 | 184 | // ... 185 | 186 | // react component 187 | // ... 188 | 189 | render () { 190 | const test = this.state.title + this.state.msg; 191 | } 192 | 193 | // ... 194 | ``` 195 | 196 | ## Development 197 | 1. Fork it 198 | 2. Create your feature branch (git checkout -b my-new-feature) 199 | 3. Commit your changes (git commit -am 'Add some feature') 200 | 4. Push to the branch (git push origin my-new-feature) 201 | 5. Create new Pull Request 202 | 203 | ## LICENSE 204 | This repo is released under the [MIT](http://opensource.org/licenses/MIT). -------------------------------------------------------------------------------- /bin/vtr: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const program = require('commander'); 4 | const chalk = require('chalk'); 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | const inquirer = require('inquirer'); 8 | 9 | const transform = require('../src/index'); 10 | const { log } = require('../src/utils'); 11 | const pkg = require('../package.json'); 12 | 13 | process.on('exit', () => console.log()); 14 | 15 | program 16 | .version(pkg.version) 17 | .usage('[options]') 18 | .option('-i, --input', 'the input path for vue component') 19 | .option('-o, --output', 'the output path for react component, which default value is process.cwd()') 20 | .option('-n, --name', 'the output file name, which default value is "react.js"') 21 | .parse(process.argv); 22 | 23 | program.on('--help', function () { 24 | console.log(); 25 | console.log(' Examples:'); 26 | console.log(); 27 | console.log(chalk.gray(' # transform a vue component to react component.')); 28 | console.log(); 29 | console.log(' $ vtr -i ./components/vue.js -o ./components/ -n react-component'); 30 | console.log(); 31 | }); 32 | 33 | function help () { 34 | if (program.args.length < 1) { 35 | return program.help(); 36 | } 37 | } 38 | 39 | help(); 40 | 41 | let src = program.args[0]; 42 | let dist = program.args[1] ? program.args[1] : process.cwd(); 43 | let name = program.args[2] ? program.args[2] : 'react.js'; 44 | 45 | src = path.resolve(process.cwd(), src); 46 | dist = path.resolve(process.cwd(), dist); 47 | 48 | if (!/(\.js|\.vue)$/.test(src)) { 49 | log(`Not support the file format: ${src}`); 50 | process.exit(); 51 | } 52 | 53 | if (!fs.existsSync(src)) { 54 | log(`The source file dose not exist: ${src}`); 55 | process.exit(); 56 | } 57 | 58 | if (!fs.statSync(src).isFile()) { 59 | log(`The source file is not a file: ${src}`); 60 | process.exit(); 61 | } 62 | 63 | if (!fs.existsSync(dist)) { 64 | log(`The dist directory path dose not exist: ${dist}`); 65 | process.exit(); 66 | } 67 | 68 | if (!/\.js$/.test(name)) { 69 | name += '.js'; 70 | } 71 | 72 | const isSFC = /\.vue$/.test(src); 73 | const targetPath = path.resolve(process.cwd(), path.join(dist, name)); 74 | 75 | if (fs.existsSync(targetPath)) { 76 | inquirer.prompt([{ 77 | type: 'confirm', 78 | message: `The file ${name} is already exists in output directory. Continue?`, 79 | name: 'ok' 80 | }]).then((answers) => { 81 | if (answers.ok) { 82 | transform(src, targetPath, isSFC); 83 | } else { 84 | process.exit(); 85 | } 86 | }); 87 | } else { 88 | transform(src, targetPath, isSFC); 89 | } 90 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-angular'], 3 | rules: { 4 | 'subject-case': [0] 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /demo/react.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | // Component Tip: https://github.com/vuejs/babel-plugin-transform-vue-jsx#component-tip 5 | import Todo from './Todo.js'; 6 | import 'path/to/vue.less'; 7 | import axios from 'axions'; 8 | export default class DemoTest extends Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | const now = Date.now(); 13 | this.state = { 14 | title: 'vue to react', 15 | msg: 'Hello world', 16 | time: now, 17 | toDolist: props.list, 18 | error: false 19 | }; 20 | } 21 | static propTypes = { 22 | name: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 23 | count: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 24 | shown: PropTypes.boolean, 25 | list: PropTypes.array, 26 | obj: PropTypes.object, 27 | level: PropTypes.oneOf([1, 2, 3]).isRequired, 28 | size: PropTypes.oneOf(['large', 'small']) 29 | }; 30 | static defaultProps = { 31 | count: 0, 32 | shown: false, 33 | list: [], 34 | obj: { test: '1111', message: 'hello' }, 35 | size: 'small' 36 | }; 37 | testMethod() { 38 | console.log('testMethod', this.props.obj); 39 | return this.state.title; 40 | } 41 | outputTitle() { 42 | const title = this.testMethod(); 43 | console.log('testMethod', title); 44 | } 45 | componentWillMount() { 46 | const prevTime = this.state.time; 47 | this.testMethod(); 48 | const msg = 'this is a test msg'; 49 | this.setState({ time: Date.now() }); 50 | console.log('mounted', msg, this.state.time); 51 | } 52 | render() { 53 | const prevTime = this.state.time; 54 | console.log('from computed', this.props.name, prevTime); 55 | const text = `${this.state.title}: ${this.state.msg}`; 56 | 57 | console.log('render'); 58 | if (this.state.error) { 59 | return

some error happend

; 60 | } 61 | 62 | return ( 63 |
64 |

{text}

65 |

Total: {this.props.count}

66 | 67 |
68 | ); 69 | } 70 | componentDidMount() { 71 | this.setState({ time: Date.now() }); 72 | console.log('mounted', this.state.time); 73 | } 74 | componentDidUpdate() { 75 | this.setState({ time: Date.now() }); 76 | console.log('updated, props prop', this.props.shown); 77 | } 78 | componentWillUnmount() { 79 | this.setState({ time: Date.now() }); 80 | console.log('beforeDestroy', this.state.time); 81 | } 82 | componentDidCatch(error, info) { 83 | this.setState({ error: true }); 84 | this.setState({ time: Date.now() }); 85 | console.log('errorCaptured', this.state.time); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /demo/sfc.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import ToDo from './todo'; 5 | import './your.less'; 6 | export default class TestSfc extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | const now = Date.now(); 11 | this.state = { 12 | list: [1, 2, 3], 13 | html: '
1111222333

ssssss

', 14 | error: false, 15 | time: now 16 | }; 17 | } 18 | static propTypes = { msg: PropTypes.string, imageSrc: PropTypes.string }; 19 | static defaultProps = { msg: 'hello, sfc' }; 20 | clickMethod() { 21 | console.log('click method'); 22 | } 23 | testMethod() { 24 | console.log('call test'); 25 | } 26 | componentWillMount() { 27 | const prevTime = this.state.time; 28 | this.testMethod(); 29 | const msg = 'this is a test msg'; 30 | this.setState({ time: Date.now() }); 31 | console.log('mounted', msg, this.state.time); 32 | } 33 | componentDidCatch(error, info) { 34 | this.setState({ error: true }); 35 | this.setState({ time: Date.now() }); 36 | console.log('errorCaptured', this.state.time); 37 | } 38 | render() { 39 | console.log('from computed', this.props.msg); 40 | const text = `${this.state.time}: ${this.state.html}`; 41 | return ( 42 |
43 |
time: {this.state.time}
44 | {this.state.error ? ( 45 |

some error happend

46 | ) : ( 47 |

your msg: {this.props.msg}

48 | )} 49 | 50 |

54 | test v-show 55 |

56 |

test v-on

57 | 58 | 68 | {text.replace(/<[^>]+>/g, '')} 69 |
70 | 71 | {this.props.msg} 72 |
73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /demo/sfc.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /demo/vue.js: -------------------------------------------------------------------------------- 1 | import axios from 'axions'; 2 | 3 | import 'path/to/vue.less'; 4 | // Component Tip: https://github.com/vuejs/babel-plugin-transform-vue-jsx#component-tip 5 | import Todo from './Todo.js'; 6 | 7 | export default { 8 | name: 'demo-test', 9 | props: { 10 | name: [String, Number], 11 | count: { 12 | type: [String, Number], 13 | default: 0 14 | }, 15 | shown: { 16 | type: Boolean, 17 | default: false 18 | }, 19 | list: { 20 | type: Array, 21 | default: () => [] 22 | }, 23 | obj: { 24 | type: Object, 25 | default: () => { 26 | return { 27 | test: '1111', 28 | message: 'hello' 29 | } 30 | } 31 | }, 32 | level: { 33 | type: Number, 34 | required: true, 35 | validator: (val) => [1, 2, 3].indexOf(val) > -1 36 | }, 37 | size: { 38 | type: String, 39 | default: 'small', 40 | validator: (val) => ['large', 'small'].indexOf(val) > -1 41 | } 42 | }, 43 | data () { 44 | const now = Date.now(); 45 | return { 46 | title: 'vue to react', 47 | msg: 'Hello world', 48 | time: now, 49 | toDolist: this.list, 50 | error: false 51 | } 52 | }, 53 | 54 | computed: { 55 | text () { 56 | const prevTime = this.time; 57 | this.test = 'sdas'; 58 | console.log('from computed', this.name, prevTime); 59 | return `${this.title}: ${this.msg}`; 60 | } 61 | }, 62 | 63 | methods: { 64 | testMethod () { 65 | console.log('testMethod', this.obj); 66 | return this.title; 67 | }, 68 | 69 | outputTitle () { 70 | const title = this.testMethod(); 71 | console.log('testMethod', title); 72 | } 73 | }, 74 | 75 | created () { 76 | const prevTime = this.time; 77 | this.testMethod(); 78 | const msg = 'this is a test msg'; 79 | this.time = Date.now(); 80 | console.log('mounted', msg, this.time); 81 | }, 82 | 83 | render () { 84 | console.log('render'); 85 | if (this.error) { 86 | return

some error happend

87 | } 88 | 89 | return ( 90 |
91 |

{this.text}

92 |

Total: {this.count}

93 | 94 |
95 | ) 96 | }, 97 | 98 | mounted () { 99 | this.time = Date.now(); 100 | console.log('mounted', this.time) 101 | }, 102 | 103 | updated () { 104 | this.time = Date.now(); 105 | console.log('updated, props prop', this.shown) 106 | }, 107 | 108 | beforeDestroy () { 109 | this.time = Date.now(); 110 | console.log('beforeDestroy', this.time); 111 | }, 112 | 113 | errorCaptured () { 114 | this.error = true; 115 | this.time = Date.now(); 116 | console.log('errorCaptured', this.time); 117 | } 118 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-to-react", 3 | "version": "1.0.0", 4 | "description": "Try to transform Vue component to React component", 5 | "author": "pomysky@gmail.com", 6 | "license": "MIT", 7 | "private": false, 8 | "bin": { 9 | "vtr": "bin/vtr" 10 | }, 11 | "files": [ 12 | "src", 13 | "bin", 14 | "LICENSE", 15 | "package.json", 16 | "README.md" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/dwqs/vue-to-react.git" 21 | }, 22 | "keywords": [ 23 | "react", 24 | "vue", 25 | "transformation", 26 | "vue-to-react" 27 | ], 28 | "bugs": { 29 | "url": "https://github.com/dwqs/vue-to-react/issues" 30 | }, 31 | "homepage": "https://github.com/dwqs/vue-to-react#readme", 32 | "scripts": { 33 | "prepush": "npm run ilint -q", 34 | "commitmsg": "npx commitlint -e", 35 | "ilint": "npx eslint src/**/*.js", 36 | "fix": "npx eslint --fix src/**/*.js" 37 | }, 38 | "dependencies": { 39 | "babel-generator": "^6.26.1", 40 | "babel-traverse": "^6.26.0", 41 | "babel-types": "^6.26.0", 42 | "babylon": "^6.18.0", 43 | "chalk": "^2.3.2", 44 | "commander": "^2.15.1", 45 | "inquirer": "^5.2.0", 46 | "prettier-eslint": "^8.8.1", 47 | "vue-template-compiler": "^2.5.16" 48 | }, 49 | "devDependencies": { 50 | "@commitlint/cli": "^6.1.3", 51 | "@commitlint/config-angular": "^6.1.3", 52 | "babel-eslint": "^8.2.2", 53 | "babel-preset-env": "^1.6.1", 54 | "eslint": "^4.18.2", 55 | "eslint-config-standard": "^11.0.0", 56 | "eslint-plugin-import": "^2.9.0", 57 | "eslint-plugin-node": "^6.0.1", 58 | "eslint-plugin-promise": "^3.7.0", 59 | "eslint-plugin-standard": "^3.0.1", 60 | "husky": "^0.14.3" 61 | }, 62 | "engines": { 63 | "node": "> 8.1.4", 64 | "npm": ">= 5.2.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/collect-state.js: -------------------------------------------------------------------------------- 1 | const babelTraverse = require('babel-traverse').default; 2 | const t = require('babel-types'); 3 | 4 | const { log } = require('./utils'); 5 | const collectVueProps = require('./vue-props'); 6 | const collectVueComputed = require('./vue-computed'); 7 | 8 | /** 9 | * Collect vue component state(data prop, props prop & computed prop) 10 | * Don't support watch prop of vue component 11 | */ 12 | exports.initProps = function initProps (ast, state) { 13 | babelTraverse(ast, { 14 | Program (path) { 15 | const nodeLists = path.node.body; 16 | let count = 0; 17 | 18 | for (let i = 0; i < nodeLists.length; i++) { 19 | const node = nodeLists[i]; 20 | // const childPath = path.get(`body.${i}`); 21 | if (t.isExportDefaultDeclaration(node)) { 22 | count++; 23 | } 24 | } 25 | 26 | if (count > 1 || !count) { 27 | const msg = !count ? 'Must hava one' : 'Only one'; 28 | log(`${msg} export default declaration in youe vue component file`); 29 | process.exit(); 30 | } 31 | }, 32 | 33 | ObjectProperty (path) { 34 | const parent = path.parentPath.parent; 35 | const name = path.node.key.name; 36 | if (parent && t.isExportDefaultDeclaration(parent)) { 37 | if (name === 'name') { 38 | if (t.isStringLiteral(path.node.value)) { 39 | state.name = path.node.value.value; 40 | } else { 41 | log(`The value of name prop should be a string literal.`); 42 | } 43 | } else if (name === 'props') { 44 | collectVueProps(path, state); 45 | path.stop(); 46 | } 47 | } 48 | } 49 | }); 50 | }; 51 | 52 | exports.initData = function initData (ast, state) { 53 | babelTraverse(ast, { 54 | ObjectMethod (path) { 55 | const parent = path.parentPath.parent; 56 | const name = path.node.key.name; 57 | 58 | if (parent && t.isExportDefaultDeclaration(parent)) { 59 | if (name === 'data') { 60 | const body = path.node.body.body; 61 | state.data['_statements'] = [].concat(body); 62 | 63 | let propNodes = {}; 64 | body.forEach(node => { 65 | if (t.isReturnStatement(node)) { 66 | propNodes = node.argument.properties; 67 | } 68 | }); 69 | 70 | propNodes.forEach(propNode => { 71 | state.data[propNode.key.name] = propNode.value; 72 | }); 73 | path.stop(); 74 | } 75 | } 76 | } 77 | }); 78 | }; 79 | 80 | exports.initComputed = function initComputed (ast, state) { 81 | babelTraverse(ast, { 82 | ObjectProperty (path) { 83 | const parent = path.parentPath.parent; 84 | const name = path.node.key.name; 85 | if (parent && t.isExportDefaultDeclaration(parent)) { 86 | if (name === 'computed') { 87 | collectVueComputed(path, state); 88 | path.stop(); 89 | } 90 | } 91 | } 92 | }); 93 | }; 94 | 95 | exports.initComponents = function initComponents (ast, state) { 96 | babelTraverse(ast, { 97 | ObjectProperty (path) { 98 | const parent = path.parentPath.parent; 99 | const name = path.node.key.name; 100 | if (parent && t.isExportDefaultDeclaration(parent)) { 101 | if (name === 'components') { 102 | // collectVueComputed(path, state); 103 | const props = path.node.value.properties; 104 | props.forEach(prop => { 105 | state.components[prop.key.name] = prop.value.name; 106 | }); 107 | path.stop(); 108 | } 109 | } 110 | } 111 | }); 112 | }; 113 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const babylon = require('babylon'); 4 | const babelTraverse = require('babel-traverse').default; 5 | const generate = require('babel-generator').default; 6 | const t = require('babel-types'); 7 | const compiler = require('vue-template-compiler'); 8 | 9 | const { initProps, initData, initComputed, initComponents } = require('./collect-state'); 10 | const { parseName, log, parseComponentName } = require('./utils'); 11 | const { 12 | genImports, genConstructor, 13 | genStaticProps, genClassMethods 14 | } = require('./react-ast-helpers'); 15 | const { 16 | collectVueProps, handleCycleMethods, 17 | handleGeneralMethods 18 | } = require('./vue-ast-helpers'); 19 | const { genSFCRenderMethod } = require('./sfc/sfc-ast-helpers'); 20 | 21 | const output = require('./output'); 22 | const traverseTemplate = require('./sfc/index'); 23 | 24 | const state = { 25 | name: undefined, 26 | data: {}, 27 | props: {}, 28 | computeds: {}, 29 | components: {} 30 | }; 31 | 32 | // Life-cycle methods relations mapping 33 | const cycle = { 34 | 'created': 'componentWillMount', 35 | 'mounted': 'componentDidMount', 36 | 'updated': 'componentDidUpdate', 37 | 'beforeDestroy': 'componentWillUnmount', 38 | 'errorCaptured': 'componentDidCatch', 39 | 'render': 'render' 40 | }; 41 | 42 | const collect = { 43 | imports: [], 44 | classMethods: {} 45 | }; 46 | 47 | function formatContent (source, isSFC) { 48 | if (isSFC) { 49 | const res = compiler.parseComponent(source, { pad: 'line' }); 50 | return { 51 | template: res.template.content.replace(/{{/g, '{').replace(/}}/g, '}'), 52 | js: res.script.content.replace(/\/\//g, '') 53 | }; 54 | } else { 55 | return { 56 | template: null, 57 | js: source 58 | }; 59 | } 60 | } 61 | 62 | // AST for vue component 63 | module.exports = function transform (src, targetPath, isSFC) { 64 | const source = fs.readFileSync(src); 65 | const component = formatContent(source.toString(), isSFC); 66 | 67 | const vast = babylon.parse(component.js, { 68 | sourceType: 'module', 69 | plugins: isSFC ? [] : ['jsx'] 70 | }); 71 | 72 | initProps(vast, state); 73 | initData(vast, state); 74 | initComputed(vast, state); 75 | initComponents(vast, state); // SFC 76 | 77 | babelTraverse(vast, { 78 | ImportDeclaration (path) { 79 | collect.imports.push(path.node); 80 | }, 81 | 82 | ObjectMethod (path) { 83 | const name = path.node.key.name; 84 | if (path.parentPath.parent.key && path.parentPath.parent.key.name === 'methods') { 85 | handleGeneralMethods(path, collect, state, name); 86 | } else if (cycle[name]) { 87 | handleCycleMethods(path, collect, state, name, cycle[name], isSFC); 88 | } else { 89 | if (name === 'data' || state.computeds[name]) { 90 | return; 91 | } 92 | log(`The ${name} method maybe be not support now`); 93 | } 94 | } 95 | }); 96 | 97 | let renderArgument = null; 98 | if (isSFC) { 99 | // traverse template in sfc 100 | renderArgument = traverseTemplate(component.template, state); 101 | } 102 | 103 | // AST for react component 104 | const tpl = `export default class ${parseName(state.name)} extends Component {}`; 105 | const rast = babylon.parse(tpl, { 106 | sourceType: 'module' 107 | }); 108 | 109 | babelTraverse(rast, { 110 | Program (path) { 111 | genImports(path, collect, state); 112 | }, 113 | 114 | ClassBody (path) { 115 | genConstructor(path, state); 116 | genStaticProps(path, state); 117 | genClassMethods(path, collect); 118 | isSFC && genSFCRenderMethod(path, state, renderArgument); 119 | } 120 | }); 121 | 122 | if (isSFC) { 123 | // replace custom element/component 124 | babelTraverse(rast, { 125 | ClassMethod (path) { 126 | if (path.node.key.name === 'render') { 127 | path.traverse({ 128 | JSXIdentifier (path) { 129 | if (t.isJSXClosingElement(path.parent) || t.isJSXOpeningElement(path.parent)) { 130 | const node = path.node; 131 | const componentName = state.components[node.name] || state.components[parseComponentName(node.name)]; 132 | if (componentName) { 133 | path.replaceWith(t.jSXIdentifier(componentName)); 134 | path.stop(); 135 | } 136 | } 137 | } 138 | }); 139 | } 140 | } 141 | }); 142 | } 143 | 144 | const { code } = generate(rast, { 145 | quotes: 'single', 146 | retainLines: true 147 | }); 148 | 149 | output(code, targetPath); 150 | log('Transform successed!!!', 'success'); 151 | }; 152 | -------------------------------------------------------------------------------- /src/output.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const format = require('prettier-eslint'); 4 | 5 | function output (code, dist) { 6 | const opts = { 7 | text: code, 8 | eslintConfig: { 9 | parserOptions: { 10 | ecmaVersion: 7, 11 | sourceType: 'module', 12 | allowImportExportEverywhere: false, 13 | ecmaFeatures: { 14 | jsx: true, 15 | modules: true 16 | } 17 | }, 18 | env: { 19 | es6: true, 20 | node: true, 21 | browser: true 22 | }, 23 | rules: { 24 | indent: [2, 2, { 'SwitchCase': 1 }], 25 | quotes: [2, 'single', { 'allowTemplateLiterals': true }], 26 | semi: [2, 'always'], 27 | eqeqeq: [2, 'always'], 28 | strict: [2, 'global'], 29 | 'object-property-newline': [2, { 'allowAllPropertiesOnSameLine': false }], 30 | 'linebreak-style': [2, 'unix'], 31 | 'object-curly-newline': [2, { 32 | 'ObjectExpression': 'always', 33 | 'ObjectPattern': 'always' 34 | }], 35 | 'no-multiple-empty-lines': [2, { max: 0 }], 36 | 'key-spacing': [2, { 'afterColon': true }], 37 | 'block-spacing': [2, 'always'], 38 | 'space-before-function-paren': [2, 'always'], 39 | 'padding-line-between-statements': [2, 40 | { 'blankLine': 'always', 'prev': 'import', 'next': 'export' } 41 | ], 42 | 'lines-around-comment': [2, { 'beforeLineComment': true }], 43 | 'no-console': 0, 44 | 'no-empty': 0, 45 | 'no-unused-vars': 0, 46 | 'no-constant-condition': 0, 47 | 'no-trailing-spaces': 0 48 | } 49 | } 50 | }; 51 | 52 | const formatCode = format(opts); 53 | // path.resolve(__dirname, '../demo/react.js') 54 | fs.writeFileSync(dist, formatCode); 55 | } 56 | 57 | module.exports = output; 58 | -------------------------------------------------------------------------------- /src/react-ast-helpers.js: -------------------------------------------------------------------------------- 1 | const t = require('babel-types'); 2 | const chalk = require('chalk'); 3 | 4 | const { genDefaultProps, genPropTypes } = require('./utils'); 5 | 6 | exports.genImports = function genImports (path, collect, state) { 7 | const nodeLists = path.node.body; 8 | const importReact = t.importDeclaration( 9 | [ 10 | t.importDefaultSpecifier(t.identifier('React')), 11 | t.importSpecifier(t.identifier('Component'), t.identifier('Component')) 12 | ], 13 | t.stringLiteral('react') 14 | ); 15 | if (Object.keys(state.props).length) { 16 | const importPropTypes = t.importDeclaration( 17 | [ 18 | t.importDefaultSpecifier(t.identifier('PropTypes')) 19 | ], 20 | t.stringLiteral('prop-types') 21 | ); 22 | collect.imports.push(importPropTypes); 23 | } 24 | collect.imports.push(importReact); 25 | collect.imports.forEach(node => nodeLists.unshift(node)); 26 | }; 27 | 28 | exports.genConstructor = function genConstructor (path, state) { 29 | const nodeLists = path.node.body; 30 | const blocks = [ 31 | t.expressionStatement(t.callExpression(t.super(), [t.identifier('props')])) 32 | ]; 33 | if (state.data['_statements']) { 34 | state.data['_statements'].forEach(node => { 35 | if (t.isReturnStatement(node)) { 36 | const props = node.argument.properties; 37 | // supports init data property with props property 38 | props.forEach(n => { 39 | if (t.isMemberExpression(n.value)) { 40 | n.value = t.memberExpression(t.identifier('props'), t.identifier(n.value.property.name)); 41 | } 42 | }); 43 | 44 | blocks.push( 45 | t.expressionStatement(t.assignmentExpression('=', t.memberExpression(t.thisExpression(), t.identifier('state')), node.argument)) 46 | ); 47 | } else { 48 | blocks.push(node); 49 | } 50 | }); 51 | } 52 | const ctro = t.classMethod( 53 | 'constructor', 54 | t.identifier('constructor'), 55 | [t.identifier('props')], 56 | t.blockStatement(blocks) 57 | ); 58 | nodeLists.push(ctro); 59 | }; 60 | 61 | exports.genStaticProps = function genStaticProps (path, state) { 62 | const props = state.props; 63 | const nodeLists = path.node.body; 64 | if (Object.keys(props).length) { 65 | nodeLists.push(genPropTypes(props)); 66 | nodeLists.push(genDefaultProps(props)); 67 | } 68 | }; 69 | 70 | exports.genClassMethods = function genClassMethods (path, collect) { 71 | const nodeLists = path.node.body; 72 | const methods = collect.classMethods; 73 | if (Object.keys(methods).length) { 74 | Object.keys(methods).forEach(key => { 75 | nodeLists.push(methods[key]); 76 | }); 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /src/sfc/directives.js: -------------------------------------------------------------------------------- 1 | const t = require('babel-types'); 2 | 3 | const { getNextJSXElment } = require('./sfc-ast-helpers'); 4 | const { log, getIdentifier } = require('../utils'); 5 | const eventMap = require('./event-map'); 6 | 7 | exports.handleIfDirective = function handleIfDirective (path, value, state) { 8 | const parentPath = path.parentPath.parentPath; 9 | const childs = parentPath.node.children; 10 | 11 | // Get JSXElment of v-else 12 | const nextElement = getNextJSXElment(parentPath); 13 | const test = state.computeds[value] ? t.identifier(value) : t.memberExpression( 14 | t.memberExpression(t.thisExpression(), getIdentifier(state, value)), 15 | t.identifier(value) 16 | ); 17 | 18 | parentPath.replaceWith( 19 | t.jSXExpressionContainer( 20 | t.conditionalExpression( 21 | test, 22 | parentPath.node, 23 | nextElement ? nextElement : t.nullLiteral() 24 | ) 25 | ) 26 | ); 27 | 28 | path.remove(); 29 | }; 30 | 31 | exports.handleShowDirective = function handleShowDirective (path, value, state) { 32 | const test = state.computeds[value] ? t.identifier(value) : t.memberExpression( 33 | t.memberExpression(t.thisExpression(), getIdentifier(state, value)), 34 | t.identifier(value) 35 | ); 36 | 37 | path.replaceWith( 38 | t.jSXAttribute( 39 | t.jSXIdentifier('style'), 40 | t.jSXExpressionContainer( 41 | t.objectExpression([ 42 | t.objectProperty( 43 | t.identifier('display'), 44 | t.conditionalExpression( 45 | test, 46 | t.stringLiteral('block'), 47 | t.stringLiteral('none') 48 | ) 49 | ) 50 | ]) 51 | ) 52 | ) 53 | ); 54 | }; 55 | 56 | exports.handleOnDirective = function handleOnDirective (path, name, value) { 57 | const eventName = eventMap[name]; 58 | if (!eventName) { 59 | log(`Not support event name`); 60 | return; 61 | } 62 | 63 | path.replaceWith( 64 | t.jSXAttribute( 65 | t.jSXIdentifier(eventName), 66 | t.jSXExpressionContainer( 67 | t.memberExpression( 68 | t.thisExpression(), 69 | t.identifier(value) 70 | ) 71 | ) 72 | ) 73 | ); 74 | }; 75 | 76 | exports.handleBindDirective = function handleBindDirective (path, name, value, state) { 77 | if (state.computeds[value]) { 78 | path.replaceWith( 79 | t.jSXAttribute( 80 | t.jSXIdentifier(name), 81 | t.jSXExpressionContainer(t.identifier(value)) 82 | ) 83 | ); 84 | return; 85 | } 86 | path.replaceWith( 87 | t.jSXAttribute( 88 | t.jSXIdentifier(name), 89 | t.jSXExpressionContainer( 90 | t.memberExpression( 91 | t.memberExpression(t.thisExpression(), getIdentifier(state, value)), 92 | t.identifier(value) 93 | ) 94 | ) 95 | ) 96 | ); 97 | }; 98 | 99 | exports.handleForDirective = function handleForDirective (path, value, definedInFor, state) { 100 | const parentPath = path.parentPath.parentPath; 101 | const childs = parentPath.node.children; 102 | const element = parentPath.node.openingElement.name.name; 103 | 104 | const a = value.split(/\s+?in\s+?/); 105 | const prop = a[1].trim(); 106 | 107 | const params = a[0].replace('(', '').replace(')', '').split(','); 108 | const newParams = []; 109 | params.forEach(item => { 110 | definedInFor.push(item.trim()); 111 | newParams.push(t.identifier(item.trim())); 112 | }); 113 | 114 | const member = state.computeds[prop] ? t.identifier(prop) : t.memberExpression( 115 | t.memberExpression(t.thisExpression(), getIdentifier(state, value)), 116 | t.identifier(prop) 117 | ); 118 | 119 | parentPath.replaceWith( 120 | t.jSXExpressionContainer( 121 | t.callExpression( 122 | t.memberExpression( 123 | member, 124 | t.identifier('map') 125 | ), 126 | [ 127 | t.arrowFunctionExpression( 128 | newParams, 129 | t.blockStatement([ 130 | t.returnStatement( 131 | t.jSXElement( 132 | t.jSXOpeningElement(t.jSXIdentifier(element), [ 133 | t.jSXAttribute( 134 | t.jSXIdentifier('key'), 135 | t.jSXExpressionContainer( 136 | t.identifier('index') 137 | ) 138 | ) 139 | ]), 140 | t.jSXClosingElement(t.jSXIdentifier(element)), 141 | childs 142 | ) 143 | ) 144 | ]) 145 | ) 146 | ] 147 | ) 148 | ) 149 | ); 150 | }; 151 | 152 | exports.handleTextDirective = function handleTextDirective (path, value, state) { 153 | const parentPath = path.parentPath.parentPath; 154 | 155 | if (state.computeds[value]) { 156 | parentPath.node.children.push( 157 | t.jSXExpressionContainer( 158 | t.callExpression( 159 | t.memberExpression( 160 | t.identifier(value), 161 | t.identifier('replace') 162 | ), 163 | [ 164 | t.regExpLiteral('<[^>]+>', 'g'), 165 | t.stringLiteral('') 166 | ] 167 | ) 168 | ) 169 | ); 170 | return; 171 | } 172 | 173 | parentPath.node.children.push( 174 | t.jSXExpressionContainer( 175 | t.callExpression( 176 | t.memberExpression( 177 | t.memberExpression( 178 | t.memberExpression(t.thisExpression(), getIdentifier(state, value)), 179 | t.identifier(value) 180 | ), 181 | t.identifier('replace') 182 | ), 183 | [ 184 | t.regExpLiteral('<[^>]+>', 'g'), 185 | t.stringLiteral('') 186 | ] 187 | ) 188 | ) 189 | ); 190 | }; 191 | 192 | exports.handleHTMLDirective = function handleHTMLDirective (path, value, state) { 193 | const val = state.computeds[value] ? t.identifier(value) : t.memberExpression( 194 | t.memberExpression(t.thisExpression(), getIdentifier(state, value)), 195 | t.identifier(value) 196 | ); 197 | 198 | path.replaceWith( 199 | t.jSXAttribute( 200 | t.jSXIdentifier('dangerouslySetInnerHTML'), 201 | t.jSXExpressionContainer( 202 | t.objectExpression( 203 | [ 204 | t.objectProperty(t.identifier('__html'), val) 205 | ] 206 | ) 207 | ) 208 | ) 209 | ) 210 | }; 211 | -------------------------------------------------------------------------------- /src/sfc/event-map.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'click': 'onClick', 3 | 'dblclick': 'onDoubleClick', 4 | 'abort': 'onAbort', 5 | 'change': 'onChange', 6 | 'input': 'onInput', 7 | 'error': 'onError', 8 | 'focus': 'onFocus', 9 | 'blur': 'onBlur', 10 | 'keydown': 'onKeyDown', 11 | 'keyup': 'onKeyUp', 12 | 'keypress': 'onKeyPress', 13 | 'load': 'onLoad', 14 | 'mousedown': 'onMouseDown', 15 | 'mouseup': 'onMouseUp', 16 | 'mousemove': 'onMouseMove', 17 | 'mouseenter': 'onMouseEnter', 18 | 'mouseleave': 'onMouseLeave', 19 | 'mouseout': 'onMouseOut', 20 | 'mouseover': 'onMouseOver', 21 | 'reset': 'onReset', 22 | 'resize': 'onResize', 23 | 'select': 'onSelect', 24 | 'submit': 'onSubmit', 25 | 'unload': 'onUnload', 26 | 'drag': 'onDrag', 27 | 'dragend': 'onDragEnd', 28 | 'dragenter': 'onDragEnter', 29 | 'dragexit': 'onDragExit', 30 | 'dragleave': 'onDragLeave', 31 | 'dragover': 'onDragOver', 32 | 'dragstart': 'onDragStart', 33 | 'drop': 'onDrop', 34 | 'touchstart': 'onTouchStart', 35 | 'touchend': 'onTouchEnd', 36 | 'touchcancel': 'onTouchCancel', 37 | 'touchmove': 'onTouchMove' 38 | }; 39 | -------------------------------------------------------------------------------- /src/sfc/index.js: -------------------------------------------------------------------------------- 1 | const babylon = require('babylon'); 2 | const t = require('babel-types'); 3 | const babelTraverse = require('babel-traverse').default; 4 | 5 | const { log, getIdentifier } = require('../utils'); 6 | const { 7 | handleIfDirective, handleShowDirective, handleOnDirective, 8 | handleForDirective, handleTextDirective, handleHTMLDirective, 9 | handleBindDirective 10 | } = require('./directives'); 11 | 12 | module.exports = function traverseTemplate (template, state) { 13 | let argument = null; 14 | // cache some variables are defined in v-for directive 15 | const definedInFor = []; 16 | 17 | // AST for template in sfc 18 | const tast = babylon.parse(template, { 19 | sourceType: 'module', 20 | plugins: ['jsx'] 21 | }); 22 | 23 | babelTraverse(tast, { 24 | ExpressionStatement: { 25 | enter (path) { 26 | 27 | }, 28 | exit (path) { 29 | argument = path.node.expression; 30 | } 31 | }, 32 | 33 | JSXAttribute (path) { 34 | const node = path.node; 35 | const value = node.value.value; 36 | 37 | if (!node.name) { 38 | return; 39 | } 40 | 41 | if (node.name.name === 'class') { 42 | path.replaceWith( 43 | t.jSXAttribute(t.jSXIdentifier('className'), node.value) 44 | ); 45 | /* eslint-disable */ 46 | return; // path.stop(); 47 | } else if (node.name.name === 'v-if') { 48 | handleIfDirective(path, value, state); 49 | } else if (node.name.name === 'v-show') { 50 | handleShowDirective(path, value, state); 51 | } else if (t.isJSXNamespacedName(node.name)) { 52 | // v-bind/v-on 53 | if (node.name.namespace.name === 'v-on') { 54 | handleOnDirective(path, node.name.name.name, value); 55 | } else if (node.name.namespace.name === 'v-bind') { 56 | handleBindDirective(path, node.name.name.name, value, state); 57 | } 58 | } else if (node.name.name === 'v-for') { 59 | handleForDirective(path, value, definedInFor, state); 60 | } else if (node.name.name === 'v-text') { 61 | handleTextDirective(path, value, state); 62 | path.remove(); 63 | } else if (node.name.name === 'v-html') { 64 | handleHTMLDirective(path, value, state); 65 | } 66 | }, 67 | 68 | JSXExpressionContainer (path) { 69 | const expression = path.node.expression; 70 | const name = expression.name; 71 | 72 | if (t.isBinaryExpression(expression)) { 73 | log('[vue-to-react]: Maybe you are using filter expression, but vtr is not supports it.'); 74 | return; 75 | } 76 | 77 | // from computed 78 | if (state.computeds[name]) { 79 | return; 80 | } 81 | 82 | // path.container: Fix replace for loop expression error 83 | if (name && !definedInFor.includes(name) && path.container) { 84 | path.replaceWith( 85 | t.jSXExpressionContainer(t.memberExpression( 86 | t.memberExpression(t.thisExpression(), getIdentifier(state, name)), 87 | t.identifier(name) 88 | )) 89 | ); 90 | // return; 91 | } 92 | } 93 | }); 94 | 95 | return argument; 96 | }; 97 | -------------------------------------------------------------------------------- /src/sfc/sfc-ast-helpers.js: -------------------------------------------------------------------------------- 1 | const t = require('babel-types'); 2 | 3 | exports.getNextJSXElment = function getNextJSXElment (path) { 4 | let nextElement = null; 5 | for (let i = path.key + 1; ; i++) { 6 | const nextPath = path.getSibling(i); 7 | if (!nextPath.node) { 8 | break; 9 | } else if (t.isJSXElement(nextPath.node)) { 10 | nextElement = nextPath.node; 11 | nextPath.traverse({ 12 | JSXAttribute (p) { 13 | if (p.node.name.name === 'v-else') { 14 | p.remove(); 15 | } 16 | } 17 | }); 18 | nextPath.remove(); 19 | break; 20 | } 21 | } 22 | 23 | return nextElement; 24 | }; 25 | 26 | exports.genSFCRenderMethod = function genSFCRenderMethod (path, state, argument) { 27 | // computed props 28 | const computedProps = Object.keys(state.computeds); 29 | let blocks = []; 30 | 31 | if (computedProps.length) { 32 | computedProps.forEach(prop => { 33 | const v = state.computeds[prop]; 34 | blocks = blocks.concat(v['_statements']); 35 | }); 36 | } 37 | blocks = blocks.concat(t.returnStatement(argument)); 38 | 39 | const render = t.classMethod( 40 | 'method', 41 | t.identifier('render'), 42 | [], 43 | t.blockStatement(blocks) 44 | ); 45 | 46 | path.node.body.push(render); 47 | }; 48 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const t = require('babel-types'); 2 | const chalk = require('chalk'); 3 | 4 | exports.parseName = function parseName (name) { 5 | name = name || 'my-react-compoennt'; 6 | const val = name.toLowerCase().split('-'); 7 | let str = ''; 8 | val.forEach(v => { 9 | v = v[0].toUpperCase() + v.substr(1); 10 | str += v; 11 | }); 12 | return str; 13 | }; 14 | 15 | exports.parseComponentName = function parseComponentName (str) { 16 | if (str) { 17 | const a = str.split('-').map(e => e[0].toUpperCase() + e.substr(1)); 18 | return a.join(''); 19 | } 20 | }; 21 | 22 | exports.log = function log (msg, type = 'error') { 23 | if (type === 'error') { 24 | return console.log(chalk.red(`[vue-to-react]: ${msg}`)); 25 | } 26 | console.log(chalk.green(msg)); 27 | }; 28 | 29 | exports.getIdentifier = function getIdentifier (state, key) { 30 | return state.data[key] ? t.identifier('state') : t.identifier('props'); 31 | }; 32 | 33 | exports.genPropTypes = function genPropTypes (props) { 34 | const properties = []; 35 | const keys = Object.keys(props); 36 | 37 | for (let i = 0, l = keys.length; i < l; i++) { 38 | const key = keys[i]; 39 | const obj = props[key]; 40 | const identifier = t.identifier(key); 41 | 42 | let val = t.memberExpression(t.identifier('PropTypes'), t.identifier('any')); 43 | if (obj.type === 'typesOfArray' || obj.type === 'array') { 44 | if (obj.type === 'typesOfArray') { 45 | const elements = []; 46 | obj.value.forEach(val => { 47 | elements.push(t.memberExpression(t.identifier('PropTypes'), t.identifier(val))); 48 | }); 49 | val = t.callExpression( 50 | t.memberExpression(t.identifier('PropTypes'), t.identifier('oneOfType')), 51 | [t.arrayExpression(elements)] 52 | ); 53 | } else { 54 | val = obj.required 55 | ? t.memberExpression(t.memberExpression(t.identifier('PropTypes'), t.identifier('array')), t.identifier('isRequired')) 56 | : t.memberExpression(t.identifier('PropTypes'), t.identifier('array')); 57 | } 58 | } else if (obj.validator) { 59 | const node = t.callExpression( 60 | t.memberExpression(t.identifier('PropTypes'), t.identifier('oneOf')), 61 | [t.arrayExpression(obj.validator.elements)] 62 | ); 63 | if (obj.required) { 64 | val = t.memberExpression( 65 | node, 66 | t.identifier('isRequired') 67 | ); 68 | } else { 69 | val = node; 70 | } 71 | } else { 72 | val = obj.required 73 | ? t.memberExpression(t.memberExpression(t.identifier('PropTypes'), t.identifier(obj.type)), t.identifier('isRequired')) 74 | : t.memberExpression(t.identifier('PropTypes'), t.identifier(obj.type)); 75 | } 76 | 77 | properties.push(t.objectProperty(identifier, val)); 78 | } 79 | 80 | // Babel does't support to create static class property??? 81 | return t.classProperty(t.identifier('static propTypes'), t.objectExpression(properties), null, []); 82 | }; 83 | 84 | exports.genDefaultProps = function genDefaultProps (props) { 85 | const properties = []; 86 | const keys = Object.keys(props).filter(key => typeof props[key].value !== 'undefined'); 87 | 88 | for (let i = 0, l = keys.length; i < l; i++) { 89 | const key = keys[i]; 90 | const obj = props[key]; 91 | const identifier = t.identifier(key); 92 | 93 | let val = t.stringLiteral('error'); 94 | if (obj.type === 'typesOfArray') { 95 | const type = typeof obj.defaultValue; 96 | if (type !== 'undefined') { 97 | const v = obj.defaultValue; 98 | val = type === 'number' ? t.numericLiteral(Number(v)) : type === 'string' ? t.stringLiteral(v) : t.booleanLiteral(v); 99 | } else { 100 | continue; 101 | } 102 | } else if (obj.type === 'array') { 103 | val = t.arrayExpression(obj.value.elements); 104 | } else if (obj.type === 'object') { 105 | val = t.objectExpression(obj.value.properties); 106 | } else { 107 | switch (obj.type) { 108 | case 'string': 109 | val = t.stringLiteral(obj.value); 110 | break; 111 | case 'boolean': 112 | val = t.booleanLiteral(obj.value); 113 | break; 114 | case 'number': 115 | val = t.numericLiteral(Number(obj.value)); 116 | break; 117 | } 118 | } 119 | 120 | properties.push(t.objectProperty(identifier, val)); 121 | } 122 | 123 | // Babel does't support to create static class property??? 124 | return t.classProperty(t.identifier('static defaultProps'), t.objectExpression(properties), null, []); 125 | }; 126 | -------------------------------------------------------------------------------- /src/vue-ast-helpers.js: -------------------------------------------------------------------------------- 1 | const t = require('babel-types'); 2 | const { log, getIdentifier } = require('./utils'); 3 | 4 | const nestedMethodsVisitor = { 5 | VariableDeclaration (path) { 6 | const declarations = path.node.declarations; 7 | declarations.forEach(d => { 8 | if (t.isMemberExpression(d.init)) { 9 | const key = d.init.property.name; 10 | d.init.object = t.memberExpression(t.thisExpression(), getIdentifier(this.state, key)); 11 | } 12 | }); 13 | this.blocks.push(path.node); 14 | }, 15 | 16 | ExpressionStatement (path) { 17 | const expression = path.node.expression; 18 | if (t.isAssignmentExpression(expression)) { 19 | const right = expression.right; 20 | const letfNode = expression.left.property; 21 | path.node.expression = t.callExpression( 22 | t.memberExpression(t.thisExpression(), t.identifier('setState')), 23 | [t.objectExpression([ 24 | t.objectProperty(letfNode, right) 25 | ])] 26 | ); 27 | } 28 | 29 | if (t.isCallExpression(expression) && !t.isThisExpression(expression.callee.object)) { 30 | path.traverse({ 31 | ThisExpression (memPath) { 32 | const key = memPath.parent.property.name; 33 | memPath.replaceWith( 34 | t.memberExpression(t.thisExpression(), getIdentifier(this.state, key)) 35 | ); 36 | memPath.stop(); 37 | } 38 | }, { state: this.state }); 39 | } 40 | 41 | this.blocks.push(path.node); 42 | }, 43 | 44 | ReturnStatement (path) { 45 | path.traverse({ 46 | ThisExpression (memPath) { 47 | const key = memPath.parent.property.name; 48 | memPath.replaceWith( 49 | t.memberExpression(t.thisExpression(), getIdentifier(this.state, key)) 50 | ); 51 | memPath.stop(); 52 | } 53 | }, { state: this.state }); 54 | this.blocks.push(path.node); 55 | } 56 | }; 57 | 58 | function createClassMethod (path, state, name) { 59 | const body = path.node.body; 60 | const blocks = []; 61 | let params = []; 62 | 63 | if (name === 'componentDidCatch') { 64 | params = [t.identifier('error'), t.identifier('info')]; 65 | } 66 | path.traverse(nestedMethodsVisitor, { blocks, state }); 67 | return t.classMethod('method', t.identifier(name), params, t.blockStatement(blocks)); 68 | } 69 | 70 | function replaceThisExpression (path, key, state) { 71 | if (state.data[key] || state.props[key]) { 72 | path.replaceWith( 73 | t.memberExpression(t.thisExpression(), getIdentifier(state, key)) 74 | ); 75 | } else { 76 | // from computed 77 | path.parentPath.replaceWith( 78 | t.identifier(key) 79 | ); 80 | } 81 | path.stop(); 82 | } 83 | 84 | function createRenderMethod (path, state, name) { 85 | if (path.node.params.length) { 86 | log(` 87 | Maybe you will call $createElement or h method in your render, but react does not support it. 88 | And it's maybe cause some unknown error in transforming 89 | `); 90 | } 91 | path.traverse({ 92 | ThisExpression (thisPath) { 93 | const parentNode = thisPath.parentPath.parentPath.parent; 94 | const isValid = t.isExpressionStatement(parentNode) || 95 | t.isVariableDeclaration(parentNode) || 96 | t.isBlockStatement(parentNode) || 97 | t.isJSXElement(parentNode) || 98 | t.isCallExpression(parentNode) || 99 | (t.isJSXAttribute(parentNode) && !parentNode.name.name.startsWith('on')); 100 | 101 | if (isValid) { 102 | // prop 103 | const key = thisPath.parent.property.name; 104 | replaceThisExpression(thisPath, key, state); 105 | } 106 | }, 107 | JSXAttribute (attrPath) { 108 | const attrNode = attrPath.node; 109 | if (attrNode.name.name === 'class') { 110 | attrPath.replaceWith( 111 | t.jSXAttribute(t.jSXIdentifier('className'), attrNode.value) 112 | ); 113 | } 114 | 115 | if (attrNode.name.name === 'domPropsInnerHTML') { 116 | const v = attrNode.value; 117 | if (t.isLiteral(v)) { 118 | attrPath.replaceWith( 119 | t.jSXAttribute( 120 | t.jSXIdentifier('dangerouslySetInnerHTML'), 121 | t.jSXExpressionContainer(t.objectExpression([t.objectProperty(t.identifier('__html'), attrNode.value)])) 122 | ) 123 | ); 124 | } else if (t.isJSXExpressionContainer(v)) { 125 | const expression = v.expression; 126 | if (t.isMemberExpression(expression)) { 127 | attrPath.traverse({ 128 | ThisExpression (thisPath) { 129 | const key = thisPath.parent.property.name; 130 | replaceThisExpression(thisPath, key, state); 131 | } 132 | }); 133 | } 134 | attrPath.replaceWith( 135 | t.jSXAttribute( 136 | t.jSXIdentifier('dangerouslySetInnerHTML'), 137 | t.jSXExpressionContainer(t.objectExpression([t.objectProperty(t.identifier('__html'), expression)])) 138 | ) 139 | ); 140 | } 141 | } 142 | } 143 | }); 144 | let blocks = []; 145 | 146 | // computed props 147 | const computedProps = Object.keys(state.computeds); 148 | if (computedProps.length) { 149 | computedProps.forEach(prop => { 150 | const v = state.computeds[prop]; 151 | blocks = blocks.concat(v['_statements']); 152 | }); 153 | } 154 | blocks = blocks.concat(path.node.body.body); 155 | return t.classMethod('method', t.identifier(name), [], t.blockStatement(blocks)); 156 | } 157 | 158 | exports.handleCycleMethods = function handleCycleMethods (path, collect, state, name, cycleName, isSFC) { 159 | if (name === 'render') { 160 | if (isSFC) { 161 | return; 162 | } 163 | collect.classMethods[cycleName] = createRenderMethod(path, state, name); 164 | } else { 165 | collect.classMethods[cycleName] = createClassMethod(path, state, cycleName); 166 | } 167 | }; 168 | 169 | exports.handleGeneralMethods = function handleGeneralMethods (path, collect, state, name) { 170 | collect.classMethods[name] = createClassMethod(path, state, name); 171 | }; 172 | -------------------------------------------------------------------------------- /src/vue-computed.js: -------------------------------------------------------------------------------- 1 | const t = require('babel-types'); 2 | const chalk = require('chalk'); 3 | 4 | const { getIdentifier, log } = require('./utils'); 5 | 6 | const nestedMethodsVisitor = { 7 | VariableDeclaration (path) { 8 | const declarations = path.node.declarations; 9 | declarations.forEach(d => { 10 | if (t.isMemberExpression(d.init)) { 11 | const key = d.init.property.name; 12 | d.init.object = t.memberExpression(t.thisExpression(), getIdentifier(this.state, key)); 13 | } 14 | }); 15 | this.statements.push(path.node); 16 | }, 17 | 18 | ExpressionStatement (path) { 19 | const expression = path.node.expression; 20 | if (t.isCallExpression(expression) && !t.isThisExpression(expression.callee.object)) { 21 | path.traverse({ 22 | ThisExpression (memPath) { 23 | const key = memPath.parent.property.name; 24 | memPath.replaceWith( 25 | t.memberExpression(t.thisExpression(), getIdentifier(this.state, key)) 26 | ); 27 | memPath.stop(); 28 | } 29 | }, { state: this.state }); 30 | } 31 | 32 | if (t.isAssignmentExpression(expression)) { 33 | return log(`Don't do assignment in ${this.key} computed prop`); 34 | } 35 | 36 | this.statements.push(path.node); 37 | }, 38 | 39 | ReturnStatement (path) { 40 | path.traverse({ 41 | ThisExpression (memPath) { 42 | const key = memPath.parent.property.name; 43 | memPath.replaceWith( 44 | t.memberExpression(t.thisExpression(), getIdentifier(this.state, key)) 45 | ); 46 | memPath.stop(); 47 | } 48 | }, { state: this.state }); 49 | const varNode = t.variableDeclaration('const', [t.variableDeclarator(t.identifier(this.key), path.node.argument)]); 50 | this.statements.push(varNode); 51 | } 52 | }; 53 | 54 | module.exports = function collectVueComputed (path, state) { 55 | const childs = path.node.value.properties; 56 | const parentKey = path.node.key.name; // computed; 57 | 58 | if (childs.length) { 59 | path.traverse({ 60 | ObjectMethod (propPath) { 61 | const parentNode = propPath.parentPath.parent; 62 | if (parentNode.key && parentNode.key.name === parentKey) { 63 | const key = propPath.node.key.name; 64 | if (!state.computeds[key]) { 65 | const body = propPath.node.key.name; 66 | const statements = []; 67 | propPath.traverse(nestedMethodsVisitor, { statements, state, key }); 68 | state.computeds[key] = { 69 | _statements: statements 70 | }; 71 | } 72 | } 73 | } 74 | }); 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/vue-props.js: -------------------------------------------------------------------------------- 1 | const t = require('babel-types'); 2 | const chalk = require('chalk'); 3 | 4 | const { log } = require('./utils'); 5 | 6 | const nestedPropsVisitor = { 7 | ObjectProperty (path) { 8 | const parentKey = path.parentPath.parent.key; 9 | if (parentKey && parentKey.name === this.childKey) { 10 | const key = path.node.key; 11 | const node = path.node.value; 12 | 13 | if (key.name === 'type') { 14 | if (t.isIdentifier(node)) { 15 | this.state.props[this.childKey].type = node.name.toLowerCase(); 16 | } else if (t.isArrayExpression(node)) { 17 | const elements = []; 18 | node.elements.forEach(n => { 19 | elements.push(n.name.toLowerCase()); 20 | }); 21 | if (!elements.length) { 22 | log(`Providing a type for the ${this.childKey} prop is a good practice.`); 23 | } 24 | /** 25 | * supports following syntax: 26 | * propKey: { type: [Number, String], default: 0} 27 | */ 28 | this.state.props[this.childKey].type = elements.length > 1 ? 'typesOfArray' : elements[0] ? elements[0].toLowerCase() : elements; 29 | this.state.props[this.childKey].value = elements.length > 1 ? elements : elements[0] ? elements[0] : elements; 30 | } else { 31 | log(`The type in ${this.childKey} prop only supports identifier or array expression, eg: Boolean, [String]`); 32 | } 33 | } 34 | 35 | if (t.isLiteral(node)) { 36 | if (key.name === 'default') { 37 | if (this.state.props[this.childKey].type === 'typesOfArray') { 38 | this.state.props[this.childKey].defaultValue = node.value; 39 | } else { 40 | this.state.props[this.childKey].value = node.value; 41 | } 42 | } 43 | 44 | if (key.name === 'required') { 45 | this.state.props[this.childKey].required = node.value; 46 | } 47 | } 48 | } 49 | }, 50 | 51 | ArrowFunctionExpression (path) { 52 | const parentKey = path.parentPath.parentPath.parent.key; 53 | if (parentKey && parentKey.name === this.childKey) { 54 | const body = path.node.body; 55 | if (t.isArrayExpression(body)) { 56 | // Array 57 | this.state.props[this.childKey].value = body; 58 | } else if (t.isBlockStatement(body)) { 59 | // Object/Block array 60 | const childNodes = body.body; 61 | if (childNodes.length === 1 && t.isReturnStatement(childNodes[0])) { 62 | this.state.props[this.childKey].value = childNodes[0].argument; 63 | } 64 | } 65 | 66 | // validator 67 | if (path.parent.key && path.parent.key.name === 'validator') { 68 | path.traverse({ 69 | ArrayExpression (path) { 70 | this.state.props[this.childKey].validator = path.node; 71 | } 72 | }, { state: this.state, childKey: this.childKey }); 73 | } 74 | } 75 | } 76 | }; 77 | 78 | module.exports = function collectVueProps (path, state) { 79 | const childs = path.node.value.properties; 80 | const parentKey = path.node.key.name; // props; 81 | 82 | if (childs.length) { 83 | path.traverse({ 84 | ObjectProperty (propPath) { 85 | const parentNode = propPath.parentPath.parent; 86 | if (parentNode.key && parentNode.key.name === parentKey) { 87 | const childNode = propPath.node; 88 | const childKey = childNode.key.name; 89 | const childVal = childNode.value; 90 | 91 | if (!state.props[childKey]) { 92 | if (t.isArrayExpression(childVal)) { 93 | const elements = []; 94 | childVal.elements.forEach(node => { 95 | elements.push(node.name.toLowerCase()); 96 | }); 97 | state.props[childKey] = { 98 | type: elements.length > 1 ? 'typesOfArray' : elements[0] ? elements[0].toLowerCase() : elements, 99 | value: elements.length > 1 ? elements : elements[0] ? elements[0] : elements, 100 | required: false, 101 | validator: false 102 | }; 103 | } else if (t.isObjectExpression(childVal)) { 104 | state.props[childKey] = { 105 | type: '', 106 | value: undefined, 107 | required: false, 108 | validator: false 109 | }; 110 | path.traverse(nestedPropsVisitor, { state, childKey }); 111 | } else if (t.isIdentifier(childVal)) { 112 | // supports propKey: type 113 | state.props[childKey] = { 114 | type: childVal.name.toLowerCase(), 115 | value: undefined, 116 | required: false, 117 | validator: false 118 | }; 119 | } else { 120 | log(`Not supports expression for the ${this.childKey} prop in props.`); 121 | } 122 | } 123 | } 124 | } 125 | }); 126 | } 127 | }; 128 | --------------------------------------------------------------------------------