├── .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 |   [](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 | 
11 |
12 | **Transform SFC Component:**
13 |
14 | 
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: '',
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 |
59 | {this.props.list.map((value, index) => {
60 | return (
61 | -
62 |
{value}
63 | {this.props.msg}
64 |
65 | );
66 | })}
67 |
68 |
{text.replace(/<[^>]+>/g, '')}
69 |
70 |
71 | {this.props.msg}
72 |
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/demo/sfc.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
time: {{time}}
4 |
some error happend
5 |
your msg: {{msg}}
6 |
test v-show
7 |
test v-on
8 |
![]()
9 |
10 | -
11 |
{{value}}
12 | {{msg}}
13 |
14 |
15 |
16 |
17 |
18 | {{msg}}
19 |
20 |
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 |
--------------------------------------------------------------------------------