├── .babelrc ├── .github └── issue_template.md ├── .gitignore ├── .npmignore ├── README.md ├── bin └── rtv ├── package.json └── src ├── class.js ├── functional.js ├── generate.js ├── index.js ├── props.js ├── save.js ├── ts.js └── utility.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["env", { 3 | "targets": { 4 | "node": "6.3.0" 5 | }, 6 | "modules": "commonjs", 7 | "debug": true 8 | }]] 9 | } 10 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | Please copy your code of React Component into the following block 2 | 3 | ``` jsx 4 | // your code 5 | 6 | ``` 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | demo 3 | lib -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo 2 | src 3 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-to-vue 2 | transform a basic react component to vue component 3 | 4 | ## Install 5 | npm install react-to-vue -g 6 | 7 | ## Usage 8 | 9 | ``` sh 10 | Usage: rtv [options] file(react component) 11 | 12 | 13 | Options: 14 | 15 | -V, --version output the version number 16 | -o --output [string] the output file name 17 | -t --ts it is a typescript component 18 | -f --flow it is a component with Flow type annotations 19 | -h, --help output usage information 20 | 21 | ``` 22 | -------------------------------------------------------------------------------- /bin/rtv: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node --harmony 2 | 'use strict' 3 | var transform = require('../lib/index.js') 4 | var path = require('path') 5 | var fs = require('fs') 6 | var chalk = require('chalk') 7 | 8 | process.env.NODE_PATH = __dirname + '/../node_modules' 9 | // 引入commander 10 | const program = require('commander') 11 | 12 | // 版本显示 13 | program.version(require('../package').version) 14 | 15 | // usage 16 | program.usage('[options] file(react component)') 17 | 18 | // ---------------------------------------- 19 | // options 20 | // ---------------------------------------- 21 | program 22 | .option('-o --output [string]', 'the output file name') 23 | .option('-t --ts', 'it is a typescript component') 24 | .option('-f --flow', 'it is a component with Flow type annotations') 25 | .parse(process.argv) 26 | 27 | 28 | if(!program.args.length){ 29 | program.help() 30 | } else { 31 | let filepath = program.args[0] 32 | filepath = path.resolve(process.cwd(), filepath) 33 | // check exists 34 | if (!fs.existsSync(filepath)) { 35 | console.log(chalk.red('file does not exist: ' + filepath)) 36 | process.exit(0) 37 | } 38 | // load options 39 | let options = {} 40 | let arr = ['output', 'ts', 'flow'] 41 | arr.forEach((item) => { 42 | program[item] !== undefined && (options[item] = program[item]) 43 | }) 44 | // load default option 45 | if (/.tsx?$/.test(filepath) && options.ts === undefined) { 46 | options.ts = true 47 | } 48 | // transform 49 | transform(filepath, options) 50 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-to-vue", 3 | "version": "1.0.11", 4 | "description": "engine that transforms React component to Vue component", 5 | "scripts": { 6 | "prepublish": "npm run start", 7 | "start": "babel ./src/ -d ./lib/", 8 | "dev": "babel ./src/ -d ./lib/ -w", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "bin": { 12 | "rtv": "bin/rtv" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+ssh://git@github.com/vicwang163/react-to-vue.git" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "vue", 21 | "transformation" 22 | ], 23 | "author": "vic.wang", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/vicwang163/react-to-vue/issues" 27 | }, 28 | "homepage": "https://github.com/vicwang163/react-to-vue#readme", 29 | "devDependencies": { 30 | "babel-cli": "^6.26.0", 31 | "babel-preset-env": "^1.6.1" 32 | }, 33 | "dependencies": { 34 | "babel-eslint": "^8.1.2", 35 | "babel-generator": "^7.0.0-beta.3", 36 | "babel-traverse": "^7.0.0-beta.3", 37 | "babylon": "^7.0.0-beta.3", 38 | "chalk": "^2.3.0", 39 | "commander": "^2.13.0", 40 | "flow-remove-types": "^1.2.3", 41 | "prettier-eslint": "^8.7.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/class.js: -------------------------------------------------------------------------------- 1 | var generate = require('babel-generator').default 2 | var babelTraverse = require('babel-traverse').default 3 | var babylon = require('babylon') 4 | var babelTypes = require('babel-types') 5 | var getProps = require('./props') 6 | const {getFunctionBody, transformSourceString, transformComponentName} = require('./utility') 7 | // autumatically increate index 8 | var refIndex = 0 9 | 10 | /* 11 | * transform setState function 12 | */ 13 | function transformSetstate (node, fileContent) { 14 | let statement = [] 15 | let args = node.expression.arguments 16 | let str = '' 17 | if (args[0]) { 18 | str = fileContent.slice(args[0].start, args[0].end) 19 | if (args[0].type === 'ObjectExpression') { 20 | args[0].properties.map(function (property) { 21 | statement.push(`this.${property.key.name} = ${fileContent.slice(property.value.start, property.value.end)}`) 22 | }) 23 | } else { 24 | str = '(' + str + ')(this, this)' 25 | statement.push(`Object.assign(this, ${str})`) 26 | } 27 | } 28 | // there exits callback 29 | if (args[1]) { 30 | let callback = fileContent.slice(args[1].start, args[1].end) 31 | statement.push(`this.$nextTick(${callback})`) 32 | } 33 | // transform source string to nodes 34 | statement = transformSourceString(statement) 35 | return statement 36 | } 37 | 38 | /* 39 | * replace setState,ref and etc 40 | */ 41 | function replaceSpecialStatement (path, fileContent) { 42 | path.traverse({ 43 | ExpressionStatement(expressPath) { 44 | let node = expressPath.node; 45 | if (!node.start) { 46 | return; 47 | } 48 | let sectionCon = fileContent.slice(node.start, node.end); 49 | let statement = ""; 50 | if (/^this\.setState/.test(sectionCon)) { 51 | // transform setState 52 | statement = transformSetstate(node, fileContent); 53 | } 54 | if (statement.length) { 55 | expressPath.replaceWithMultiple(statement); 56 | } 57 | }, 58 | MemberExpression (memPath) { 59 | let node = memPath.node 60 | if (node.property.name === 'refs') { 61 | if (node.object.type === 'ThisExpression') { 62 | node.property.name = '$refs' 63 | } 64 | } 65 | // replace `this.state.xx` with `this.xx` 66 | if (['state', 'props'].includes(node.property.name)) { 67 | if (node.object.type === 'ThisExpression') { 68 | memPath.replaceWith(babelTypes.thisExpression()) 69 | } 70 | } 71 | }, 72 | JSXAttribute (attrPath) { 73 | let node = attrPath.node 74 | if (node.name.name === 'className') { 75 | node.name.name = 'class' 76 | } else if (node.name.name === 'dangerouslySetInnerHTML') { 77 | node.name.name = 'domPropsInnerHTML' 78 | let expression = attrPath.get('value.expression') 79 | if (expression.isIdentifier()) { 80 | expression.replaceWithSourceString(`${expression.node.name}.__html`) 81 | } else { 82 | expression.replaceWith(expression.get('properties.0.value')) 83 | } 84 | } 85 | } 86 | }); 87 | } 88 | 89 | // parse constructor 90 | function parseConstructor (path, fileContent, result, root) { 91 | let paramName = path.get('params.0') ? path.get('params.0').node.name : null 92 | path.traverse({ 93 | ExpressionStatement (expressPath) { 94 | let node = expressPath.node 95 | let sectionCon = fileContent.slice(node.start, node.end) 96 | if (/^super|\.bind\(this\)/.test(sectionCon)) { 97 | expressPath.remove() 98 | return 99 | } 100 | // retrieve variables 101 | if (/^this\.state/.test(sectionCon)) { 102 | expressPath.traverse({ 103 | ObjectExpression (objPath) { 104 | let properties = objPath.node.properties 105 | for (let i = 0; i < properties.length; i++) { 106 | let property = properties[i] 107 | let value = fileContent.slice(property.value.start, property.value.end) 108 | // validate if it exists in the props 109 | if (root.propTypes && root.propTypes[result.componentName] && root.propTypes[result.componentName][property.key.name]) { 110 | root.caveats.push(`The data property "${property.key.name}" is already declared as a prop, please redesign this component`) 111 | } else { 112 | result.data[property.key.name] = value.replace(/this\.props/g, 'this').replace(/props/g, 'this') 113 | } 114 | } 115 | } 116 | }) 117 | expressPath.remove() 118 | } 119 | }, 120 | MemberExpression (memPath) { 121 | // replace this.props.xx or props.xx 122 | let node = memPath.node 123 | if (babelTypes.isThisExpression(node.object) && ['state', 'props'].includes(node.property.name)) { 124 | memPath.replaceWith(babelTypes.thisExpression()) 125 | } else if (paramName && node.object.name === paramName) { 126 | node.object.name = 'this' 127 | } 128 | } 129 | }) 130 | // put this code into `created` lifecycle 131 | let code = getFunctionBody(path.node.body) 132 | if (code.trim()) { 133 | result.lifeCycles['created'] = code 134 | } 135 | } 136 | // parse life cycle methods 137 | function parseLifeCycle (path, method, fileContent, result) { 138 | // replace special statement 139 | replaceSpecialStatement(path, fileContent) 140 | // debugger 141 | let code = getFunctionBody(path.node.body) 142 | result.lifeCycles[method] = code 143 | } 144 | 145 | // parse events 146 | function parseMethods (path, fileContent, result) { 147 | // replace special statement 148 | replaceSpecialStatement(path, fileContent) 149 | // generate method 150 | let code = getFunctionBody(path.node.body); 151 | let method = path.node.key.name 152 | let params = path.node.params 153 | let paramsArr = [] 154 | for (let i = 0; i < params.length; i++) { 155 | paramsArr.push(fileContent.slice(params[i].start, params[i].end)) 156 | } 157 | code = `${method} (${paramsArr.join(', ')}) {${code}}` 158 | result.methods.push(code) 159 | } 160 | 161 | // parse render 162 | function parseRender (path, fileContent, result) { 163 | // retrieve special properties 164 | path.traverse({ 165 | JSXElement (jsxPath) { 166 | let element = jsxPath.node.openingElement 167 | // find sub component 168 | if (element.name && element.name.name && /^[A-Z]/.test(element.name.name)) { 169 | result.components.push(element.name.name) 170 | let name = transformComponentName(element.name.name) 171 | element.name.name = name 172 | if (jsxPath.node.closingElement) { 173 | jsxPath.node.closingElement.name.name = name 174 | } 175 | } 176 | }, 177 | JSXAttribute (attrPath) { 178 | let node = attrPath.node 179 | // if value of ref property is callback, we need to change it 180 | if (node.name.name === 'ref' && node.value.type !== 'StringLiteral') { 181 | let value = node.value 182 | let code 183 | // automatically increase the value 184 | let refValue = 'vueref' + refIndex++ 185 | let bodys = null 186 | // only has one statement 187 | if ((bodys = attrPath.get('value.expression.body'), bodys) && bodys.isAssignmentExpression()) { 188 | code = fileContent.slice(bodys.node.left.start, bodys.node.left.end) 189 | code = `${code} = this.$refs.${refValue}` 190 | } else if (bodys.node && (bodys = attrPath.get('value.expression.body.body'), bodys) && bodys.length === 1) { // only has one statement 191 | // only has one statement in the blockstatement 192 | bodys = bodys[0].get('expression.left') 193 | code = fileContent.slice(bodys.node.start, bodys.node.end) 194 | code = `${code} = this.$refs.${refValue}` 195 | } else { 196 | code = fileContent.slice(value.expression.start, value.expression.end) 197 | code = `(${code})(this.$refs.${refValue})` 198 | } 199 | code += ';' 200 | 201 | let jsxContainer = attrPath.get('value') 202 | if (jsxContainer) { 203 | jsxContainer.replaceWith(babelTypes.stringLiteral(refValue)) 204 | } 205 | // add the ref callback code into specified lifecycle 206 | result.lifeCycles.mounted = code + (result.lifeCycles.mounted ? result.lifeCycles.mounted : '') 207 | result.lifeCycles.updated = code + (result.lifeCycles.updated ? result.lifeCycles.updated : '') 208 | // result.lifeCycles.destroyed = unmountCode + (result.lifeCycles.destroyed ? result.lifeCycles.destroyed : '') 209 | } else if (node.name.name === 'className') { 210 | node.name.name = 'class' 211 | } else if (node.name.name === 'dangerouslySetInnerHTML') { 212 | // replace dangerouslySetInnerHTML with domPropsInnerHTML 213 | node.name.name = 'domPropsInnerHTML' 214 | let expression = attrPath.get('value.expression') 215 | if (expression.isIdentifier()) { 216 | expression.replaceWithSourceString(`${expression.node.name}.__html`) 217 | } else { 218 | expression.replaceWith(expression.get('properties.0.value')) 219 | } 220 | } 221 | }, 222 | MemberExpression (memPath) { 223 | // change `this.state` and `this.props` to `this` 224 | let node = memPath.node 225 | // replace this.props.children with 'this.$slots.default' 226 | if (node.property.name === 'children' && node.object.object && node.object.object.type === 'ThisExpression') { 227 | node.property.name = 'default' 228 | node.object.property.name = '$slots' 229 | } 230 | if (['state', 'props'].includes(node.property.name)) { 231 | if (node.object.type === 'ThisExpression') { 232 | memPath.replaceWith(babelTypes.thisExpression()) 233 | } 234 | } 235 | } 236 | }) 237 | let code = getFunctionBody(path.node.body); 238 | result.render = `render () {${code}}` 239 | } 240 | 241 | /* 242 | * replace static variables and methods 243 | */ 244 | function replaceStatic (path, root) { 245 | path.traverse({ 246 | MemberExpression (memPath) { 247 | let propertyName = memPath.node.property.name 248 | let memExpression = root.source.slice(memPath.node.object.start, memPath.node.object.end) 249 | if (root.class.static[propertyName] && ['this.constructor', root.class.componentName].includes(memExpression)) { 250 | memPath.replaceWithSourceString(`static_${propertyName}`) 251 | } 252 | } 253 | }) 254 | } 255 | 256 | module.exports = function getClass (path, fileContent, root) { 257 | Object.assign(root.class, { 258 | static: {}, 259 | data: {}, 260 | methods: [], 261 | lifeCycles: {}, 262 | components: [], 263 | componentName: path.node.id.name 264 | }) 265 | let result = root.class 266 | 267 | path.traverse({ 268 | ClassMethod (path) { 269 | // replace statics 270 | replaceStatic(path, root) 271 | // deal with different method 272 | switch(path.node.key.name) { 273 | case 'constructor': 274 | parseConstructor(path, fileContent, result, root); 275 | break; 276 | case 'componentWillMount': 277 | parseLifeCycle(path, 'beforeMount', fileContent, result); 278 | break; 279 | case 'componentDidMount': 280 | parseLifeCycle(path, 'mounted', fileContent, result); 281 | break; 282 | case 'componentWillUpdate': 283 | parseLifeCycle(path, 'beforeUpdate', fileContent, result); 284 | break; 285 | case 'componentDidUpdate': 286 | parseLifeCycle(path, 'updated', fileContent, result); 287 | break; 288 | case 'componentWillUnmount': 289 | parseLifeCycle(path, 'destroyed', fileContent, result); 290 | break; 291 | case 'componentDidCatch': 292 | parseLifeCycle(path, 'errorCaptured', fileContent, result); 293 | break; 294 | case 'shouldComponentUpdate': 295 | case 'componentWillReceiveProps': 296 | break; 297 | case 'render': 298 | parseRender(path, fileContent, result); 299 | break; 300 | default: 301 | parseMethods(path, fileContent, result); 302 | break; 303 | } 304 | }, 305 | ClassProperty (path) { 306 | let node = path.node 307 | if (node.key && ['defaultProps', 'propTypes'].includes(node.key.name)) { 308 | getProps(result.componentName, node.key.name, node.value, root) 309 | } else if (node.static) { 310 | if (node.value) { 311 | result.static[node.key.name] = root.source.slice(node.value.start, node.value.end) 312 | } else { 313 | result.static[node.key.name] = null 314 | } 315 | } 316 | } 317 | }) 318 | return result 319 | } -------------------------------------------------------------------------------- /src/functional.js: -------------------------------------------------------------------------------- 1 | const {reportIssue, transformSourceString, getFunctionBody, transformComponentName} = require('./utility') 2 | const generate = require('babel-generator').default 3 | 4 | module.exports = function (path, fileContent, result, funcType = null) { 5 | let funcCom = { 6 | components: [], 7 | functional: true, 8 | componentName: funcType === 'arrow' ? path.parentPath.node.id.name : path.node.id.name 9 | } 10 | let extraCode = '' 11 | let paramsPath = path.get('params.0') 12 | let originalPropName = '' 13 | if (!paramsPath) { 14 | // it means there is no params 15 | } else if (paramsPath.isObjectPattern()) { 16 | let node = paramsPath.node 17 | extraCode = `let ${fileContent.slice(node.start, node.end)} = c.props` 18 | } else if (paramsPath.isAssignmentPattern()) { 19 | let node = paramsPath.node.left 20 | extraCode = `let ${fileContent.slice(node.start, node.end)} = c.props` 21 | } else if (paramsPath.isIdentifier()) { 22 | // record original prop name 23 | originalPropName = paramsPath.node.name 24 | extraCode = `const ${originalPropName} = c.props` 25 | } else { 26 | reportIssue(`Unknow params for '${funcCom.componentName}'`) 27 | } 28 | 29 | // retrieve sub component 30 | path.traverse({ 31 | JSXElement (jsxPath) { 32 | let element = jsxPath.node.openingElement 33 | // find sub component 34 | if (element.name && element.name.name && /^[A-Z]/.test(element.name.name)) { 35 | funcCom.components.push(element.name.name) 36 | let name = transformComponentName(element.name.name) 37 | element.name.name = name 38 | if (jsxPath.node.closingElement) { 39 | jsxPath.node.closingElement.name.name = name 40 | } 41 | } 42 | }, 43 | MemberExpression (memPath) { 44 | if (memPath.node.property.name === 'children' && memPath.node.object.name === originalPropName) { 45 | memPath.node.object.name = 'c' 46 | } 47 | }, 48 | JSXAttribute (attrPath) { 49 | let node = attrPath.node 50 | if (node.name.name === 'className') { 51 | node.name.name = 'class' 52 | } else if (node.name.name === 'dangerouslySetInnerHTML') { 53 | node.name.name = 'domPropsInnerHTML' 54 | let expression = attrPath.get('value.expression') 55 | if (expression.isIdentifier()) { 56 | expression.replaceWithSourceString(`${expression.node.name}.__html`) 57 | } else { 58 | expression.replaceWith(expression.get('properties.0.value')) 59 | } 60 | } 61 | } 62 | }) 63 | 64 | if (funcCom.componentName !== result.exportName) { 65 | // get code 66 | let code = getFunctionBody(path, false) 67 | //if it's a common function 68 | result.functional.push(code) 69 | return 70 | } else if (extraCode) { 71 | //add the extra code into blockstatement 72 | let astFrag = transformSourceString(extraCode) 73 | path.get('body.body.0').insertBefore(astFrag) 74 | } 75 | 76 | // get code 77 | let code = getFunctionBody(path.get('body')) 78 | funcCom.render = `render (h, c) {${code}}` 79 | // add funcCom into result 80 | result.functional.push(funcCom) 81 | } 82 | -------------------------------------------------------------------------------- /src/generate.js: -------------------------------------------------------------------------------- 1 | var format = require("prettier-eslint"); 2 | var babelTraverse = require('babel-traverse').default 3 | var {transformComponentName} = require('./utility') 4 | 5 | function mergeExportComponent (object) { 6 | let com = null; 7 | object.functional.forEach((func, index) => { 8 | if (func.functional) { 9 | com = func 10 | // remove functional component 11 | object.functional.splice(index, 1) 12 | } 13 | }) 14 | if (!com) { 15 | com = object.class 16 | } 17 | return com 18 | } 19 | 20 | module.exports = function generateVueComponent (object) { 21 | let content = '' 22 | // add imports 23 | object.import.forEach((item) => { 24 | content += item + '\n' 25 | }) 26 | 27 | // add variable declaration 28 | object.declaration.forEach((item) => { 29 | content += item + '\n' 30 | }) 31 | content += '\n\n' 32 | 33 | // merge export component 34 | let component = mergeExportComponent(object) 35 | 36 | // generate common function 37 | object.functional.forEach((func) => { 38 | // common function 39 | content += func 40 | }) 41 | 42 | // generate export component 43 | if (component && component.render) { 44 | // add class static variables and methods if exists 45 | if (component.static) { 46 | for (let name in component.static) { 47 | if (component.static[name]) { 48 | content += `let static_${name} = ${component.static[name]}\n` 49 | } else { 50 | content += `let static_${name}\n` 51 | } 52 | } 53 | } 54 | // vueProps is designed to put vue properties 55 | let vueProps = [] 56 | content += '// export component\n' 57 | content += 'export default {\n' 58 | 59 | // add component name 60 | if (component.componentName) { 61 | vueProps.push(`name: '${transformComponentName(component.componentName)}'`) 62 | } 63 | 64 | // add functional tag if it's a functional component 65 | if (component.functional) { 66 | vueProps.push(`functional: true`) 67 | } 68 | 69 | // add props 70 | if (object.propTypes && object.propTypes[component.componentName]) { 71 | let props = object.propTypes[component.componentName] 72 | let defaultValues = object.defaultProps && object.defaultProps[component.componentName] 73 | let propArr = [] 74 | for (let item in props) { 75 | let value = props[item] 76 | if (defaultValues && defaultValues[item]) { 77 | value.default = defaultValues[item] 78 | } 79 | let arr = [] 80 | for (let key in value) { 81 | if (key === 'type') { 82 | arr.push(`${key}: ${value[key]}`) 83 | } else if (key === 'required') { 84 | arr.push(`${key}: ${value[key]}`) 85 | } else { 86 | arr.push(`${key}: ${ value.type === 'String' ? `'${value[key]}'` : value[key] }`) 87 | } 88 | } 89 | propArr.push(`${item}: {${arr.join(',\n')}}`) 90 | } 91 | vueProps.push(`props: {${propArr.join(',\n')}}`) 92 | } 93 | // add data 94 | if (component.data && Object.keys(component.data).length) { 95 | let data = component.data 96 | let arr = [] 97 | for (let key in data) { 98 | arr.push(`${key}: ${data[key]}`) 99 | } 100 | let value = `return {${arr.join(',\n')}}` 101 | vueProps.push(`data () {${value}}`) 102 | } 103 | 104 | // add methods 105 | if (component.methods && component.methods.length) { 106 | vueProps.push(`methods: {${component.methods.join(',')}}`) 107 | } 108 | 109 | // add life cycles 110 | if (component.lifeCycles && Object.keys(component.lifeCycles).length) { 111 | let lifeCycles = [] 112 | for (let key in component.lifeCycles) { 113 | lifeCycles.push(`${key} () {${component.lifeCycles[key]}}`) 114 | } 115 | vueProps.push(`${lifeCycles.join(',')}`) 116 | } 117 | 118 | // add sub components 119 | if (component.components) { 120 | let result = [] 121 | // validate components 122 | component.components.forEach(function (com) { 123 | let exist = object.import.some(function (value) { 124 | return value.includes(com) 125 | }) 126 | if (exist) { 127 | result.push(com) 128 | } 129 | }) 130 | // if exists necessary components 131 | if (result.length) { 132 | vueProps.push(`components: {${result.join(',')}}`) 133 | } 134 | } 135 | 136 | // add render 137 | if (component.render) { 138 | vueProps.push(`${component.render}`) 139 | } 140 | // generate component 141 | content += vueProps.join(',\n') + '}' 142 | } 143 | 144 | // format content 145 | const options = { 146 | text: content, 147 | eslintConfig: { 148 | parser: 'babel-eslint', 149 | rules: { 150 | semi: ["error", "never"], 151 | quotes: ["error", "single"], 152 | "no-extra-semi": 2, 153 | "max-len": ["error", { "code": 150 }], 154 | "object-curly-spacing": ["error", "never"], 155 | "space-before-function-paren": ["error", "always"], 156 | "no-multiple-empty-lines": ["error", { "max": 0}], 157 | "line-comment-position": ["error", { "position": "beside" }] 158 | } 159 | } 160 | }; 161 | content = format(options); 162 | return content 163 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var getProps = require('./props') 4 | var getClass = require('./class') 5 | var saveComponent = require('./save') 6 | var generateVueComponent = require('./generate') 7 | var getFunctional = require('./functional') 8 | var babelTraverse = require('babel-traverse').default 9 | var babylon = require('babylon') 10 | var chalk = require('chalk') 11 | var transformTS = require('./ts') 12 | var flowRemoveTypes = require('flow-remove-types'); 13 | var {reportIssue, removeBadCode, isVariableFunc} = require('./utility') 14 | 15 | module.exports = function transform (src, options) { 16 | // read file 17 | let fileContent = fs.readFileSync(src) 18 | fileContent = fileContent.toString() 19 | // hard code 20 | fileContent = removeBadCode(fileContent) 21 | // if it is used with Flow type annotations 22 | if (options.flow) { 23 | fileContent = flowRemoveTypes(fileContent).toString() 24 | } 25 | // parse module 26 | let ast = babylon.parse(fileContent, { 27 | sourceType:'module', 28 | plugins: ["typescript", "classProperties", "jsx", "trailingFunctionCommas", "asyncFunctions", "exponentiationOperator", "asyncGenerators", "objectRestSpread", "decorators"] 29 | }) 30 | if (options.ts) { 31 | transformTS(ast) 32 | } 33 | // fix trailingComments issues with hard code 34 | babelTraverse(ast, { 35 | BlockStatement (path) { 36 | path.node.body.forEach((item) => { 37 | if (item.trailingComments && fileContent.charCodeAt([item.end]) === 10) { 38 | delete item.trailingComments 39 | } 40 | }) 41 | } 42 | }) 43 | // traverse module 44 | let result = { 45 | "import": [], 46 | "declaration": [], 47 | "class": {}, 48 | "functional": [], 49 | "propTypes": {}, 50 | "defaultProps": {}, 51 | // there exists incompatibility 52 | "caveats": [], 53 | "source": fileContent 54 | } 55 | babelTraverse(ast, { 56 | Program (path) { 57 | let nodeLists = path.node.body 58 | let classDefineCount = 0 59 | for (let i = 0; i < nodeLists.length; i++) { 60 | let node = nodeLists[i] 61 | let cPath = path.get(`body.${i}`) 62 | // get prop-types 63 | if (cPath.isExpressionStatement() && node.expression.type === 'AssignmentExpression') { 64 | let leftNode = node.expression.left 65 | if (leftNode.type === 'MemberExpression' && ["defaultProps", "propTypes"].includes(leftNode.property.name)) { 66 | let className = node.expression.left.object.name 67 | getProps(className, leftNode.property.name, node.expression.right, result) 68 | } 69 | } else if (cPath.isClassDeclaration()) { 70 | classDefineCount ++ 71 | if (classDefineCount > 1) { 72 | console.error('One file should have only one class declaration!') 73 | process.exit() 74 | } 75 | } else if (cPath.isExportDefaultDeclaration()) { 76 | result.exportName = node.declaration.name ? node.declaration.name : node.declaration.id.name 77 | } else if (cPath.isVariableDeclaration() && !isVariableFunc(cPath)) { 78 | // it's just simple variable declaration, e.g. `let a = 1` 79 | result.declaration.push(fileContent.slice(node.start, node.end)) 80 | } 81 | } 82 | }, 83 | ImportDeclaration (path) { 84 | let node = path.node 85 | // skip react and prop-types modules 86 | if (["react", "prop-types", "react-dom"].includes(node.source.value)) { 87 | return 88 | } 89 | result.import.push(fileContent.slice(node.start, node.end)) 90 | }, 91 | ClassDeclaration (path) { 92 | if (path.parentPath.type !== 'Program' && path.parentPath.type !== 'ExportDefaultDeclaration') { 93 | reportIssue('This component seems like HOC or something else, we may not support it') 94 | } 95 | if (path.node.decorators) { 96 | result.caveats.push('react-to-vue does not support decorator for now') 97 | } 98 | getClass(path, fileContent, result) 99 | }, 100 | FunctionDeclaration (path) { 101 | if (path.parentPath.type !== 'Program') { 102 | return 103 | } 104 | // retrieve functional component 105 | getFunctional(path, fileContent, result) 106 | }, 107 | ArrowFunctionExpression (path) { 108 | let variablePath = path.findParent((p) => p.isVariableDeclaration()) 109 | if (!variablePath || variablePath.parentPath.type !== 'Program' || path.getPathLocation().split('.').length > 4) { 110 | return 111 | } 112 | // retrieve functional component 113 | getFunctional(path, fileContent, result, 'arrow') 114 | } 115 | }) 116 | // check props validation 117 | if (!Object.keys(result.propTypes).length && /props/.test(fileContent)) { 118 | result.caveats.push(`There is no props validation, please check it manually`) 119 | } 120 | // generate vue component according to object 121 | let output = generateVueComponent(result) 122 | 123 | // save file 124 | saveComponent(options.output, output) 125 | 126 | // output caveats 127 | if (result.caveats.length) { 128 | console.log(chalk.red("Caveats:")); 129 | console.log(chalk.red(result.caveats.join('\n'))) 130 | } 131 | } -------------------------------------------------------------------------------- /src/props.js: -------------------------------------------------------------------------------- 1 | // valid types that can be transformed to Vue types 2 | const VALIDTYPES = { 3 | array: 'Array', 4 | bool: 'Boolean', 5 | func: 'Function', 6 | number: 'Number', 7 | object: 'Object', 8 | string: 'String', 9 | symbol: 'Symbol' 10 | } 11 | 12 | module.exports = function (className, category, node, root) { 13 | let result = null 14 | result = root[category][className] = {} 15 | // prop-types 16 | if (category === 'propTypes') { 17 | // properties loop 18 | let properties = node.properties 19 | for (let i = 0; i < properties.length; i++) { 20 | let property = properties[i] 21 | // get value of proptypes 22 | let value = property.value.property ? property.value.property.name : null 23 | if (property.value.property && (value === 'isRequired' || VALIDTYPES[value])) { 24 | // case: propTypes.string.isRequired 25 | if (value === 'isRequired') { 26 | result[property.key.name] = { 27 | type: VALIDTYPES[property.value.object.property.name], 28 | required: true 29 | } 30 | } else { 31 | result[property.key.name] = { 32 | type: VALIDTYPES[value] 33 | } 34 | } 35 | } else { 36 | // if it's not the specific types, default use `Object` type 37 | result[property.key.name] = {type: 'Object'} 38 | // add this proptype into caveats 39 | root.caveats.push(`Inconsistent propTypes: '${className}:${property.key.name}'`) 40 | } 41 | } 42 | } else { 43 | // component name 44 | let propTypeObj 45 | if (!root['propTypes'][className]) { 46 | propTypeObj = root['propTypes'][className] = {} 47 | } else { 48 | propTypeObj = root['propTypes'][className] 49 | } 50 | // properties loop 51 | let properties = node.properties 52 | for (let i = 0; i < properties.length; i++) { 53 | let property = properties[i] 54 | result[property.key.name] = property.value.value !== undefined ? property.value.value : root.source.slice(property.value.start, property.value.end) 55 | // check if propTypes exist 56 | if (!propTypeObj[property.key.name]) { 57 | property.value.type.replace(/^[A-Z][a-z]+/, function (value) { 58 | propTypeObj[property.key.name] = {type: value} 59 | }) 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/save.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | 4 | module.exports = function (dst, output) { 5 | if (dst) { 6 | if (!path.isAbsolute(dst)) { 7 | dst = path.resolve(process.cwd(), dst) 8 | } 9 | fs.writeFileSync(dst, output) 10 | } else { 11 | console.log(output) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ts.js: -------------------------------------------------------------------------------- 1 | var babelTraverse = require('babel-traverse').default 2 | 3 | module.exports = function (ast) { 4 | babelTraverse(ast,{ 5 | ExportNamedDeclaration (exportPath) { 6 | let declaration = exportPath.get('declaration') 7 | if (declaration && ( declaration.isTSInterfaceDeclaration() || declaration.isTSTypeAliasDeclaration())) { 8 | exportPath.remove() 9 | } 10 | }, 11 | TSTypeParameterInstantiation (path) { 12 | path.remove() 13 | }, 14 | TSTypeAnnotation (path) { 15 | path.remove() 16 | }, 17 | TSAsExpression (path) { 18 | path.replaceWith(path.get('expression')) 19 | } 20 | }) 21 | return ast 22 | } -------------------------------------------------------------------------------- /src/utility.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const babelTraverse = require('babel-traverse').default 3 | const babylon = require('babylon') 4 | const generate = require('babel-generator').default 5 | 6 | export function reportIssue (msg) { 7 | msg && console.log(msg) 8 | console.log(chalk.red('Please report issue here:') + chalk.underline.red('https://github.com/vicwang163/react-to-vue/issues')) 9 | process.exit() 10 | } 11 | 12 | /* 13 | * transform source string to ast nodes 14 | */ 15 | export function transformSourceString (statement) { 16 | if (!Array.isArray(statement)) { 17 | statement = [statement] 18 | } 19 | let result = [] 20 | for (let i = 0; i < statement.length; i++) { 21 | let replacement = statement[i] 22 | replacement = babylon.parse(replacement) 23 | replacement = replacement.program.body[0] 24 | result.push(babelTraverse.removeProperties(replacement)) 25 | } 26 | return result 27 | } 28 | 29 | /* 30 | * transform component name 31 | */ 32 | export function transformComponentName (name) { 33 | if (/[A-Z]{2,}/.test(name)) { 34 | return name 35 | } 36 | return name.replace(/^[A-Z]/, v => v.toLowerCase()).replace(/[A-Z]/g, v => '-' + v.toLowerCase()) 37 | } 38 | 39 | /* 40 | * generate BlockStatement 41 | */ 42 | export function getFunctionBody (node, removeBrace = true) { 43 | let tempAst = babylon.parse('{console.log(1)}') 44 | let executed = false 45 | let rt 46 | babelTraverse(tempAst, { 47 | BlockStatement (tempPath) { 48 | if (executed) { 49 | return 50 | } 51 | executed = true 52 | tempPath.replaceWith(node) 53 | } 54 | }) 55 | rt = generate(tempAst, {}) 56 | rt = rt.code 57 | removeBrace && (rt = rt.replace(/^{|}$/g, '')) 58 | return rt 59 | } 60 | 61 | /* 62 | * remove bad code with hard code for now 63 | */ 64 | export function removeBadCode (con) { 65 | return con.replace(/\.\.\.(\w+),\n/, function (a, v) {return '...' + v + '\n'}) 66 | } 67 | 68 | /* 69 | * check if the VariableDeclaration is function, like 'let a = function () {}' 70 | */ 71 | 72 | export function isVariableFunc (path) { 73 | let result = false 74 | path.traverse({ 75 | "ArrowFunctionExpression|FunctionDeclaration" (p) { 76 | result = true 77 | p.stop() 78 | } 79 | }) 80 | return result 81 | } 82 | --------------------------------------------------------------------------------