├── .gitignore ├── README.md ├── api.js ├── index.js ├── package.json ├── utils.js ├── view.js └── w2a ├── api.js └── view.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # transform-miniprogram 2 | 用来转换微信和支付宝小程序 3 | 4 | ## 接口 5 | 6 | #### transformMiniprogram(form, cb) 7 | 8 | 参数说明 9 | 10 | 属性 | 说明 11 | ---- | ---- 12 | form | 表单属性 13 | cb | 回调函数 14 | 15 | form表单参数说明 16 | 17 | 属性 | 说明 18 | ---- | ---- 19 | src | 微信或者支付宝小程序的项目目录 20 | type | 目前只支持w2a 21 | dist | 默认是src同级目录下的文件夹,自动生成 22 | 23 | ## 运行原理 24 | 25 | 1.首先通过copy项目,生成额外的项目地址,在copy过程中,会把项目文件名改成相应的文件 26 | 2.转换js文件,里面babel来转换接口 27 | 3.用html-tool工具来转换view -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | var glob = require("glob") 2 | var babylon = require('babylon') 3 | var traverse = require('babel-traverse').default 4 | var generate = require("babel-generator").default 5 | var utils = require('./utils') 6 | 7 | module.exports = function * transformApi (form, transformLogs) { 8 | // 过滤js文件 9 | var files = yield new Promise((resolve) => { 10 | glob(form.dist + "/**/*.js", {ignore: '**/node_modules/**/*.js'}, function (err, files) { 11 | resolve(err ? [] :files) 12 | }) 13 | }) 14 | var api = require('./' + form.type + '/api') 15 | // 用于转换context 16 | var transformedCtx = api.__transformCtx__ 17 | var i = 0 18 | var content 19 | // 遍历文件进行转换 20 | for(i = 0; i < files.length; i++) { 21 | content = yield utils.getContent(files[i]) 22 | var result = babylon.parse(content, { 23 | sourceType:'module', 24 | plugins: '*' 25 | }) 26 | // 转换api接口 27 | traverse(result, { 28 | MemberExpression (path) { 29 | var node = path.node 30 | var ctx = node.object.name 31 | var method = node.property.name 32 | if (ctx && method && api[ctx]) { 33 | // 如果在api列表里面存在,表示需要更新参数,函数名等 34 | if (api[ctx][method] != undefined) { 35 | // 只有tips 36 | if (api[ctx][method].tips) { 37 | // 增加transform logs 38 | transformLogs.push({ 39 | type: 'tips', 40 | file: files[i], 41 | row: node.loc.start.line, 42 | column: node.loc.start.column, 43 | message: ctx + '.' + method + ':' + api[ctx][method].tips 44 | }) 45 | } else { // 需要转换 46 | var mappingName = api[ctx][method].mapping ? api[ctx][method].mapping : method 47 | var sourceCode 48 | var afterCode 49 | if (path.parent.type !== 'CallExpression' || !api[ctx][method].params) { 50 | // 只要替换ctx和函数名即可 51 | path.replaceWithSourceString(transformedCtx[ctx] + '.' + mappingName) 52 | sourceCode = ctx + '.' + method 53 | afterCode = transformedCtx[ctx] + '.' + mappingName 54 | } else { 55 | // 需要替换ctx,函数名和参数 56 | sourceCode = content.slice(path.parent.start, path.parent.end) 57 | // 替换ctx,函数名 58 | afterCode = sourceCode.replace(ctx + '.' + method, transformedCtx[ctx] + '.' + mappingName) 59 | if (api[ctx][method].params) { 60 | for (var pKey in api[ctx][method].params) { 61 | afterCode = afterCode.replace(pKey, api[ctx][method]['params'][pKey]) 62 | } 63 | } 64 | path.parentPath.replaceWithSourceString(afterCode) 65 | } 66 | // 增加transform logs 67 | transformLogs.push({ 68 | file: files[i], 69 | row: node.loc.start.line, 70 | column: node.loc.start.column, 71 | before: sourceCode, 72 | after: afterCode 73 | }) 74 | } 75 | } else if (api[ctx][method] === null) { 76 | // 表示没有响应的接口 77 | // 增加transform logs 78 | transformLogs.push({ 79 | type: 'error', 80 | file: files[i], 81 | row: node.loc.start.line, 82 | column: node.loc.start.column, 83 | message: ctx + '.' + method + ':没有相对应的函数' 84 | }) 85 | } else { 86 | // 否则只要修改context 87 | path.replaceWithSourceString(transformedCtx[ctx] + '.' + method) 88 | // 增加transform logs 89 | transformLogs.push({ 90 | file: files[i], 91 | row: node.loc.start.line, 92 | column: node.loc.start.column, 93 | before: ctx + '.' + method, 94 | after: transformedCtx[ctx] + '.' + method 95 | }) 96 | } 97 | } 98 | } 99 | }) 100 | result = generate(result, {}) 101 | yield utils.saveFile(files[i], result.code) 102 | } 103 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var transformApi = require('./api') 2 | var transformView = require('./view') 3 | var utils = require('./utils') 4 | var path = require('path') 5 | var co = require('co') 6 | 7 | module.exports = function transformMiniprogram (form, cb) { 8 | // 日志变量 9 | var transformLogs = [] 10 | if (!["w2a"].includes(form.type)) { 11 | return 12 | } 13 | // 指定转换目录 14 | form.dist = path.join(path.dirname(form.src), path.basename(form.src) + '_alipay') 15 | 16 | co(function * () { 17 | yield utils.copyProject(form.src, form.dist) 18 | yield transformApi(form, transformLogs) 19 | yield transformView(form, transformLogs) 20 | }).then(function () { 21 | cb(null, transformLogs) 22 | }).catch(function (e) { 23 | cb(e) 24 | }) 25 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transform-miniprogram", 3 | "version": "1.0.0", 4 | "description": "transform miniprogram", 5 | "main": "index.js", 6 | "dependencies": { 7 | "babel-generator": "^6.26.0", 8 | "babel-traverse": "^6.26.0", 9 | "babylon": "^6.18.0", 10 | "co": "^4.6.0", 11 | "html-tool": "^0.0.1", 12 | "recursive-copy": "^2.0.7" 13 | }, 14 | "devDependencies": {}, 15 | "scripts": { 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/vicwang163/transform-miniprogram.git" 21 | }, 22 | "keywords": [ 23 | "miniprogram" 24 | ], 25 | "author": "vic.wang", 26 | "license": "ISC", 27 | "bugs": { 28 | "url": "https://github.com/vicwang163/transform-miniprogram/issues" 29 | }, 30 | "homepage": "https://github.com/vicwang163/transform-miniprogram#readme" 31 | } 32 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var recursiveCopy = require('recursive-copy') 3 | var path = require('path') 4 | 5 | // 复制项目 6 | exports.copyProject = function (fromPath, toPath) { 7 | var lists = fs.readdirSync(fromPath).filter(function (item) { 8 | return !/(node_modules|DS_store)/i.test(item) 9 | }); 10 | var options = { 11 | overwrite: true, 12 | expand: true, 13 | dot: true, 14 | rename: function(filePath) { 15 | if (/wxml/.test(filePath)) { 16 | return filePath.replace(/wxml$/, 'axml') 17 | } else if (/wxss/.test(filePath)) { 18 | return filePath.replace(/wxss$/, 'axss') 19 | } else { 20 | return filePath 21 | } 22 | } 23 | } 24 | var arr = [] 25 | for (var i = 0; i < lists.length; i++) { 26 | arr.push(recursiveCopy(path.join(fromPath, lists[i]), path.join(toPath, lists[i].replace(/wxml$/, 'axml').replace(/wxss$/, 'axss')), options)) 27 | } 28 | return Promise.all(arr) 29 | } 30 | 31 | // 获取内容 32 | exports.getContent = function (filepath) { 33 | return new Promise(function (resolve) { 34 | fs.readFile(filepath, function (err, con) { 35 | resolve(con.toString()) 36 | }) 37 | }) 38 | } 39 | 40 | // 写内容 41 | exports.saveFile = function (path, con) { 42 | return new Promise((resolve, reject) => { 43 | fs.writeFile(path, con, (error) => { 44 | if (error) { 45 | reject(error) 46 | } else { 47 | resolve(true) 48 | } 49 | }) 50 | }) 51 | } -------------------------------------------------------------------------------- /view.js: -------------------------------------------------------------------------------- 1 | var glob = require("glob") 2 | var utils = require('./utils') 3 | var htmlTool = require('html-tool') 4 | 5 | module.exports = function * transformView (form, transformLogs) { 6 | var files = yield new Promise((resolve) => { 7 | glob(form.dist + "/**/*." + (form.type === "w2a" ? 'axml' : 'wxml'), function (err, files) { 8 | resolve(err ? [] :files) 9 | }) 10 | }) 11 | 12 | var viewObject = require('./' + form.type + '/view') 13 | var content 14 | var tree 15 | // 遍历文件进行转换 16 | for(i = 0; i < files.length; i++) { 17 | content = yield utils.getContent(files[i]) 18 | tree = htmlTool.parse(content) 19 | htmlTool.traverse(tree, function (node) { 20 | var name = node.name 21 | var attrs = node.attribs 22 | var key 23 | // 如果存在需要转换的组件 24 | if (viewObject[name]) { 25 | // 替换组件名 26 | if (viewObject[name].mapping) { 27 | node.name = viewObject[name].mapping 28 | transformLogs.push({ 29 | file: files[i], 30 | before: '原来组件名:' + name, 31 | after: '现在组件名:' + viewObject[name].mapping 32 | }) 33 | } 34 | // 查找属性 35 | var newAttrs = [] 36 | for (key in attrs) { 37 | if (viewObject[name]['attrs'][key] === null) { 38 | transformLogs.push({ 39 | file: files[i], 40 | type: 'error', 41 | message: '['+ name +']:没有属性"' + key + '"' 42 | }) 43 | } else if (viewObject[name]['attrs'][key]) { 44 | // 赋值新的 45 | newAttrs[viewObject[name]['attrs'][key]] = attrs[key] 46 | // add logs 47 | transformLogs.push({ 48 | file: files[i], 49 | before: '['+ name +']:' + key, 50 | after: '['+ name +']:' + viewObject[name]['attrs'][key] 51 | }) 52 | } else { 53 | newAttrs[key] = attrs[key] 54 | } 55 | } 56 | // 赋值 57 | node.attribs = newAttrs 58 | } else { 59 | // add logs 60 | transformLogs.push({ 61 | file: files[i], 62 | type: 'error', 63 | message: '['+ name +']:组件不存在' 64 | }) 65 | } 66 | }) 67 | // generate html 68 | content = htmlTool.generate(tree) 69 | // save file 70 | yield utils.saveFile(files[i], content) 71 | } 72 | } -------------------------------------------------------------------------------- /w2a/api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 1.如果值为null,表示不存在该接口 3 | * 2.如果有值, 4 | * a. 其中有tips,表示给予的建议 5 | * b. 有mapping,需要转换相应的接口名 6 | * c. 如果有params,则需要转换相应的参数 7 | * 8 | */ 9 | module.exports = { 10 | __transformCtx__: { 11 | wx: 'my' 12 | }, 13 | wx: { 14 | request: { 15 | mapping: 'httpRequest', 16 | params: { 17 | header: 'headers' 18 | } 19 | }, 20 | uploadFile: { 21 | params: { 22 | name: 'fileName' 23 | } 24 | }, 25 | connectSocket: { 26 | params: { 27 | header: 'headers', 28 | protocols: '', 29 | method: '' 30 | } 31 | }, 32 | closeSocket: { 33 | params: { 34 | code: '', 35 | reason: '' 36 | } 37 | }, 38 | onSocketClose: null, 39 | chooseImage: { 40 | params: { 41 | sizeType: '' 42 | } 43 | }, 44 | getImageInfo: null, 45 | saveImageToPhotosAlbum: { 46 | mapping: 'saveImage', 47 | params: { 48 | filePath: 'url' 49 | } 50 | }, 51 | startRecord: null, 52 | stopRecord: null, 53 | getRecorderManager: null, 54 | getBackgroundAudioPlayerState: null, 55 | playBackgroundAudio: null, 56 | pauseBackgroundAudio: null, 57 | seekBackgroundAudio: null, 58 | stopBackgroundAudio: null, 59 | onBackgroundAudioPlay: null, 60 | onBackgroundAudioPause: null, 61 | onBackgroundAudioStop: null, 62 | getBackgroundAudioManager: null, 63 | createAudioContext: null, 64 | createInnerAudioContext: null, 65 | chooseVideo: null, 66 | saveVideoToPhotosAlbum: null, 67 | createVideoContext: null, 68 | createCameraContext: null, 69 | saveFile: null, 70 | getSavedFileList: null, 71 | getSavedFileInfo: null, 72 | removeSavedFile: null, 73 | openDocument: null, 74 | chooseLocation: null, 75 | onAccelerometerChange: null, 76 | startAccelerometer: null, 77 | stopAccelerometer: null, 78 | onCompassChange: null, 79 | startCompass: null, 80 | stopCompass: null, 81 | makePhoneCall: { 82 | params: { 83 | phoneNumber: 'number' 84 | } 85 | }, 86 | scanCode: null, 87 | setClipboardData: { 88 | mapping: 'setClipboard', 89 | params: { 90 | data: 'text' 91 | } 92 | }, 93 | getClipboardData: { 94 | mapping: 'getClipboard' 95 | }, 96 | createBLEConnection: { 97 | mapping: 'connectBLEDevice' 98 | }, 99 | closeBLEConnection: { 100 | mapping: 'disconnectBLEDevice' 101 | }, 102 | startBeaconDiscovery: null, 103 | stopBeaconDiscovery: null, 104 | getBeacons: null, 105 | onBeaconUpdate: null, 106 | onBeaconServiceChange: null, 107 | setScreenBrightness: null, 108 | getScreenBrightness: null, 109 | setKeepScreenOn: null, 110 | onUserCaptureScreen: null, 111 | vibrateLong: { 112 | mapping: 'vibrate' 113 | }, 114 | vibrateShort: { 115 | mapping: 'vibrate' 116 | }, 117 | addPhoneContact: null, 118 | showToast: { 119 | params: { 120 | title: 'content', 121 | icon: '', 122 | image: '', 123 | mask: '' 124 | } 125 | }, 126 | showLoading: { 127 | params: { 128 | title: 'content', 129 | mask: '' 130 | } 131 | }, 132 | showModal: { 133 | tips: '请使用支付宝小程序的alert,confirm等接口' 134 | }, 135 | showActionSheet: { 136 | itemList: 'items', 137 | itemColor: '' 138 | }, 139 | setTopBarText: { 140 | tips: '请使用支付宝小程序的setNavigationBar' 141 | }, 142 | setNavigationBarTitle: { 143 | tips: '请使用支付宝小程序的setNavigationBar' 144 | }, 145 | setNavigationBarColor: { 146 | tips: '请使用支付宝小程序的setNavigationBar' 147 | }, 148 | switchTab: null, 149 | getExtConfig: null, 150 | getExtConfigSync: null, 151 | login: { 152 | tips: '请查阅支付宝小程序的getAuthCode接口' 153 | }, 154 | checkSession: { 155 | tips: '请查阅支付宝小程序的getAuthCode接口' 156 | }, 157 | authorize: { 158 | tips: '请查阅支付宝小程序的getAuthCode接口' 159 | }, 160 | getUserInfo: { 161 | tips: '请查阅支付宝小程序的getAuthUserInfo接口' 162 | }, 163 | requestPayment: { 164 | tips: '请查阅支付宝小程序的tradePay接口' 165 | }, 166 | showShareMenu: null, 167 | hideShareMenu: null, 168 | updateShareMenu: null, 169 | getShareInfo: null 170 | } 171 | } -------------------------------------------------------------------------------- /w2a/view.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | view: { 3 | attrs: { 4 | 'hover-stop-propagation': null 5 | } 6 | }, 7 | text: { 8 | attrs: { 9 | 'space': null, 10 | 'decode': null 11 | } 12 | }, 13 | progress: { 14 | attrs: { 15 | color: null 16 | } 17 | }, 18 | button: { 19 | attrs: { 20 | 'open-type': null, 21 | 'hover-stop-propagation': null, 22 | 'bindgetuserinfo': null, 23 | 'session-from': null, 24 | 'send-message-title': null, 25 | 'send-message-path': null, 26 | 'send-message-img': null, 27 | 'show-message-card': null, 28 | 'bindcontact': null, 29 | 'bindgetphonenumber': null 30 | } 31 | }, 32 | 'checkbox-group': { 33 | attrs: { 34 | 'bindchange': null 35 | } 36 | }, 37 | 'checkbox': { 38 | attrs: { 39 | color: null 40 | } 41 | }, 42 | 'form': { 43 | attrs: { 44 | 'report-submit': null, 45 | 'bindsubmit': null, 46 | 'bindreset': null 47 | } 48 | }, 49 | 'input': { 50 | attrs: { 51 | 'placeholder-style': null, 52 | 'placeholder-class': null, 53 | 'cursor-spacing': null, 54 | 'confirm-type': null, 55 | 'confirm-hold': null, 56 | 'bindinput': 'onInput', 57 | 'bindfocus': 'onFocus', 58 | 'bindblur': 'onBlur', 59 | 'bindconfirm': 'onConfirm' 60 | } 61 | }, 62 | 'radio-group': { 63 | attrs: { 64 | 'bindchange': null 65 | } 66 | }, 67 | 'radio': { 68 | attrs: { 69 | color: null 70 | } 71 | }, 72 | slider: { 73 | attrs: { 74 | color: null 75 | } 76 | }, 77 | 'switch': { 78 | attrs: { 79 | type: null, 80 | bindchange: 'onChange' 81 | } 82 | }, 83 | textarea: { 84 | attrs: { 85 | 'placeholder-style': null, 86 | 'placeholder-class': null, 87 | 'fixed': null, 88 | 'cursor-spacing': null, 89 | 'cursor': null, 90 | 'bindinput': 'onInput', 91 | 'bindfocus': 'onFocus', 92 | 'bindblur': 'onBlur', 93 | 'bindconfirm': 'onConfirm' 94 | } 95 | }, 96 | navigator: { 97 | attrs: { 98 | delta: null, 99 | 'hover-stop-propagation': null 100 | } 101 | }, 102 | image: { 103 | attrs: { 104 | 'lazy-load': null, 105 | 'binderror': 'onError', 106 | 'bindload': 'onLoad' 107 | } 108 | }, 109 | map: { 110 | attrs: { 111 | 'bindmarkertap': 'onMarkerTap', 112 | 'bindcallouttap': 'onCallouttap', 113 | 'bindcontroltap': 'onControltap', 114 | 'bindregionchange': 'onRegionchange', 115 | 'bindtap': 'onTap' 116 | } 117 | } 118 | } --------------------------------------------------------------------------------