├── index.js ├── test ├── cache.test.js ├── custom-component │ ├── upgrade.test.js │ └── init.test.js ├── miniprogram │ ├── upgrade.test.js │ └── init.test.js ├── game │ └── init.test.js └── plugin │ └── init.test.js ├── .gitignore ├── src ├── config.js ├── quickstart │ └── init.js ├── custom-component │ ├── init.js │ └── upgrade.js ├── utils.js └── cli.js ├── package.json ├── README.md ├── LICENSE └── .eslintrc.js /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/cli') 2 | -------------------------------------------------------------------------------- /test/cache.test.js: -------------------------------------------------------------------------------- 1 | process.argv.push('cache') // show 2 | process.argv.push('--clear') // clear 3 | 4 | require('../src/cli') 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | package-lock.json 4 | 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | node_modules 12 | coverage 13 | test/**/demo -------------------------------------------------------------------------------- /test/custom-component/upgrade.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | process.argv.push('upgrade') 4 | process.argv.push(path.join(__dirname, './demo')) 5 | // process.argv.push('--force') // 强制更新脚手架 6 | 7 | require('../../src/cli') -------------------------------------------------------------------------------- /test/miniprogram/upgrade.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | process.argv.push('upgrade') 4 | process.argv.push(path.join(__dirname, './demo')) 5 | // process.argv.push('--force') // 强制更新脚手架 6 | 7 | require('../../src/cli') 8 | -------------------------------------------------------------------------------- /test/game/init.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | process.argv.push('init') 4 | process.argv.push(path.join(__dirname, './demo')) 5 | // process.argv.push('--force') // 强制初始化,就算已存在项目代码 6 | // process.argv.push('--newest') // 先拉取最新脚手架再初始化 7 | 8 | process.argv.push('--type') 9 | process.argv.push('game') 10 | 11 | require('../../src/cli') -------------------------------------------------------------------------------- /test/plugin/init.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | process.argv.push('init') 4 | process.argv.push(path.join(__dirname, './demo')) 5 | // process.argv.push('--force') // 强制初始化,就算已存在项目代码 6 | // process.argv.push('--newest') // 先拉取最新脚手架再初始化 7 | 8 | process.argv.push('--type') 9 | process.argv.push('plugin') 10 | 11 | require('../../src/cli') -------------------------------------------------------------------------------- /test/miniprogram/init.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | process.argv.push('init') 4 | process.argv.push(path.join(__dirname, './demo')) 5 | // process.argv.push('--force') // 强制初始化,就算已存在项目代码 6 | // process.argv.push('--newest') // 先拉取最新脚手架再初始化 7 | 8 | process.argv.push('--type') 9 | process.argv.push('miniprogram') 10 | 11 | require('../../src/cli') -------------------------------------------------------------------------------- /test/custom-component/init.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | process.argv.push('init') 4 | process.argv.push(path.join(__dirname, './demo')) 5 | // process.argv.push('--force') // 强制初始化,就算已存在项目代码 6 | // process.argv.push('--newest') // 先拉取最新脚手架再初始化 7 | 8 | process.argv.push('--type') 9 | process.argv.push('custom-component') 10 | 11 | require('../../src/cli') -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | customComponent: { 3 | name: 'custom-component', 4 | download: 'https://github.com/wechat-miniprogram/miniprogram-custom-component/archive/master.zip', 5 | }, 6 | 7 | miniprogram: { 8 | name: 'miniprogram', 9 | download: 'https://github.com/wechat-miniprogram/miniprogram-quickstart/archive/master.zip', 10 | }, 11 | 12 | game: { 13 | name: 'game', 14 | download: 'https://github.com/wechat-miniprogram/miniprogram-game-quickstart/archive/master.zip', 15 | }, 16 | 17 | plugin: { 18 | name: 'plugin', 19 | download: 'https://github.com/wechat-miniprogram/miniprogram-plugin-quickstart/archive/master.zip', 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wechat-miniprogram/miniprogram-cli", 3 | "version": "0.0.5", 4 | "description": "miniprogram custom component cli", 5 | "bin": { 6 | "miniprogram": "src/cli.js" 7 | }, 8 | "main": "index.js", 9 | "scripts": { 10 | "lint": "eslint src/*" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/wechat-miniprogram/miniprogram-cli.git" 15 | }, 16 | "author": "wechat-miniprogram", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "eslint": "^5.3.0", 20 | "eslint-config-airbnb-base": "13.1.0", 21 | "eslint-plugin-import": "^2.14.0", 22 | "eslint-plugin-node": "^7.0.1", 23 | "eslint-plugin-promise": "^3.8.0" 24 | }, 25 | "dependencies": { 26 | "commander": "^2.17.1", 27 | "download": "^7.1.0", 28 | "glob": "^7.1.2", 29 | "inquirer": "^6.1.0", 30 | "progress": "^2.0.0", 31 | "rimraf": "^2.6.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # miniprogram-cli 2 | 3 | 小程序开发相关命令行工具,目前支持以下几种模板: 4 | 5 | * 第三方自定义组件(custom-component) 6 | * 小程序 quickstart(miniprogram) 7 | * 小程序插件 quickstart(plugin) 8 | * 小游戏 quickstart(game) 9 | 10 | ## 安装 11 | 12 | ``` 13 | npm install -g @wechat-miniprogram/miniprogram-cli 14 | ``` 15 | 16 | ## 初始化 17 | 18 | ``` 19 | miniprogram init [options] [dirPath] 20 | ``` 21 | 22 | 根据模板来进行项目的初始化 23 | 24 | 支持 options 如下: 25 | 26 | | option | 描述 | 27 | |---|---| 28 | | -t, --type | 项目的初始化所使用的模板 | 29 | | -f, --force | 强制初始化项目,可能会覆盖掉目录中已存在的项目 | 30 | | -p, --proxy | 下载/更新模板时的请求代理 | 31 | | -n, --newest | 使用线上最新的模板进行项目的初始化 | 32 | 33 | ## 升级 34 | 35 | ``` 36 | miniprogram upgrade [options] [dirPath] 37 | ``` 38 | 39 | 根据最新模板对已有项目进行升级。目前仅支持**自定义组件**项目。 40 | 41 | 支持 options 如下: 42 | 43 | | option | 描述 | 44 | |---|---| 45 | | -f, --force | 强制升级项目,会覆盖掉原有项目中的构建相关文件 | 46 | | -p, --proxy | 下载/更新模板时的请求代理 | 47 | 48 | ## 缓存 49 | 50 | ``` 51 | miniprogram cache [options] 52 | ``` 53 | 54 | 显示缓存目录。 55 | 56 | 支持 options 如下: 57 | 58 | | option | 描述 | 59 | |---|---| 60 | | -c, --clear | 清空缓存的模板 | 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/quickstart/init.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const _ = require('../utils') 4 | const configMap = require('../config') 5 | 6 | const templateDir = _.getTemplateDir() 7 | 8 | /** 9 | * 拷贝所有文件 10 | */ 11 | async function copy(templateProject, dirPath) { 12 | const globOptions = { 13 | cwd: templateProject, 14 | nodir: true, 15 | dot: true, 16 | } 17 | 18 | const allFiles = await _.globSync('**/*', globOptions) 19 | 20 | for (let i = 0, len = allFiles.length; i < len; i++) { 21 | const filePath = allFiles[i] 22 | // eslint-disable-next-line no-await-in-loop 23 | await _.copyFile(path.join(templateProject, filePath), path.join(dirPath, filePath)) 24 | } 25 | } 26 | 27 | /** 28 | * 执行初始化命令 29 | */ 30 | async function init(dirPath, options) { 31 | const config = configMap[options.type] 32 | const templateProject = path.join(templateDir, config.name) 33 | 34 | if (options.newest) { 35 | // 删除模板,为了拉取新模板 36 | await _.removeDir(templateProject) 37 | } 38 | 39 | // 拉取模板 40 | await _.downloadTemplate(config, options.proxy) 41 | 42 | const isTemlateExist = await _.checkDirExist(templateProject) 43 | 44 | if (!isTemlateExist) { 45 | // eslint-disable-next-line no-console 46 | console.log('can not download the template project, please check your internet connection.') 47 | process.exit(1) 48 | } 49 | 50 | await _.recursiveMkdir(dirPath) 51 | 52 | await copy(templateProject, dirPath) 53 | } 54 | 55 | module.exports = function (dirPath, options = {}) { 56 | init(dirPath, options) 57 | // eslint-disable-next-line no-console 58 | .then(() => console.log(`[init done]: ${dirPath}`)) 59 | // eslint-disable-next-line no-console 60 | .catch(err => console.error(err)) 61 | } 62 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'extends': [ 3 | 'airbnb-base', 4 | 'plugin:promise/recommended' 5 | ], 6 | 'parserOptions': { 7 | 'ecmaVersion': 9, 8 | 'ecmaFeatures': { 9 | 'jsx': false 10 | }, 11 | 'sourceType': 'module' 12 | }, 13 | 'env': { 14 | 'es6': true, 15 | 'node': true, 16 | 'jest': true 17 | }, 18 | 'plugins': [ 19 | 'import', 20 | 'node', 21 | 'promise' 22 | ], 23 | 'rules': { 24 | 'arrow-parens': 'off', 25 | 'comma-dangle': [ 26 | 'error', 27 | 'only-multiline' 28 | ], 29 | 'complexity': ['error', 10], 30 | 'func-names': 'off', 31 | 'global-require': 'off', 32 | 'handle-callback-err': [ 33 | 'error', 34 | '^(err|error)$' 35 | ], 36 | 'import/no-unresolved': [ 37 | 'error', 38 | { 39 | 'caseSensitive': true, 40 | 'commonjs': true, 41 | 'ignore': ['^[^.]'] 42 | } 43 | ], 44 | 'import/prefer-default-export': 'off', 45 | 'linebreak-style': 'off', 46 | 'no-catch-shadow': 'error', 47 | 'no-continue': 'off', 48 | 'no-div-regex': 'warn', 49 | 'no-else-return': 'off', 50 | 'no-param-reassign': 'off', 51 | 'no-plusplus': 'off', 52 | 'no-shadow': 'off', 53 | 'no-multi-assign': 'off', 54 | 'no-underscore-dangle': 'off', 55 | 'node/no-deprecated-api': 'error', 56 | 'node/process-exit-as-throw': 'error', 57 | 'object-curly-spacing': [ 58 | 'error', 59 | 'never' 60 | ], 61 | 'operator-linebreak': [ 62 | 'error', 63 | 'after', 64 | { 65 | 'overrides': { 66 | ':': 'before', 67 | '?': 'before' 68 | } 69 | } 70 | ], 71 | 'prefer-arrow-callback': 'off', 72 | 'prefer-destructuring': 'off', 73 | 'prefer-template': 'off', 74 | 'quote-props': [ 75 | 1, 76 | 'as-needed', 77 | { 78 | 'unnecessary': true 79 | } 80 | ], 81 | 'semi': [ 82 | 'error', 83 | 'never' 84 | ] 85 | }, 86 | 'globals': {} 87 | } 88 | -------------------------------------------------------------------------------- /src/custom-component/init.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const _ = require('../utils') 4 | const config = require('../config').customComponent 5 | 6 | const now = new Date() 7 | const templateDir = _.getTemplateDir() 8 | const templateProject = path.join(templateDir, config.name) 9 | const globOptions = { 10 | cwd: templateProject, 11 | nodir: true, 12 | dot: true, 13 | } 14 | 15 | /** 16 | * 拷贝 package.json 17 | */ 18 | async function copyPackageJson(dirPath, options) { 19 | let content = await _.readFile(path.join(templateProject, 'package.json')) 20 | 21 | content = content.replace(/("name": ")(?:.*)(")/ig, `$1${options.name}$2`) 22 | content = content.replace(/("version": ")(?:.*)(")/ig, `$1${options.version}$2`) 23 | content = content.replace(/("url": ")(?:.*)(")/ig, `$1${options.git}$2`) 24 | content = content.replace(/("author": ")(?:.*)(")/ig, `$1${options.author}$2`) 25 | content = content.replace(/("miniprogram": ")(?:.*)(")/ig, `$1${options.dist}$2`) 26 | content = content.replace(/("main": ")(?:.*)(")/ig, `$1${options.dist}/index.js$2`) 27 | 28 | await _.writeFile(path.join(dirPath, 'package.json'), content) 29 | } 30 | 31 | /** 32 | * 拷贝 license 33 | */ 34 | async function copyLicense(dirPath, options) { 35 | let content = await _.readFile(path.join(templateProject, 'LICENSE')) 36 | 37 | content = content.replace(/(Copyright\s+\(c\)\s+)(?:.*)(\s*[\r\n])/ig, `$1${now.getFullYear()} ${options.author}$2`) 38 | 39 | await _.writeFile(path.join(dirPath, 'LICENSE'), content) 40 | } 41 | 42 | /** 43 | * 拷贝其他文件 44 | */ 45 | async function copyOthers(dirPath) { 46 | // src 目录 47 | const srcFiles = await _.globSync('src/**/*', globOptions) 48 | 49 | // test 目录 50 | const testFiles = await _.globSync('test/**/*', globOptions) 51 | 52 | // tools 目录 53 | const toolsFiles = await _.globSync('tools/**/*', globOptions) 54 | 55 | // 其他根目录下的文件,如 .gitignore 等 56 | let rootFiles = await _.globSync('*', globOptions) 57 | rootFiles = rootFiles.filter(toolsFile => toolsFile.slice(-12) !== 'package.json' && toolsFile.slice(-7) !== 'LICENSE') 58 | 59 | const allFiles = [].concat(srcFiles, testFiles, toolsFiles, rootFiles) 60 | for (let i = 0, len = allFiles.length; i < len; i++) { 61 | const filePath = allFiles[i] 62 | // eslint-disable-next-line no-await-in-loop 63 | await _.copyFile(path.join(templateProject, filePath), path.join(dirPath, filePath)) 64 | } 65 | } 66 | 67 | /** 68 | * 执行初始化命令 69 | */ 70 | async function init(dirPath, options) { 71 | if (options.newest) { 72 | // 删除模板,为了拉取新模板 73 | await _.removeDir(templateProject) 74 | } 75 | 76 | // 拉取模板 77 | await _.downloadTemplate(config, options.proxy) 78 | 79 | const isTemlateExist = await _.checkDirExist(templateProject) 80 | 81 | if (!isTemlateExist) { 82 | // eslint-disable-next-line no-console 83 | console.log('can not download the template project, please check your internet connection.') 84 | process.exit(1) 85 | } 86 | 87 | await _.recursiveMkdir(dirPath) 88 | 89 | await copyPackageJson(dirPath, options) 90 | await copyLicense(dirPath, options) 91 | 92 | await copyOthers(dirPath) 93 | } 94 | 95 | module.exports = function (dirPath, options = {}) { 96 | init(dirPath, options) 97 | // eslint-disable-next-line no-console 98 | .then(() => console.log(`[init done]: ${dirPath}`)) 99 | // eslint-disable-next-line no-console 100 | .catch(err => console.error(err)) 101 | } 102 | -------------------------------------------------------------------------------- /src/custom-component/upgrade.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const _ = require('../utils') 4 | const config = require('../config').customComponent 5 | 6 | const templateDir = _.getTemplateDir() 7 | const templateProject = path.join(templateDir, config.name) 8 | const globOptions = { 9 | cwd: templateProject, 10 | nodir: true, 11 | dot: true, 12 | } 13 | 14 | /** 15 | * check override 16 | */ 17 | function checkOverride(type, override) { 18 | for (let i = 0, len = override.length; i < len; i++) { 19 | if (override[i].indexOf(type) === 0) return true 20 | } 21 | 22 | return false 23 | } 24 | 25 | /** 26 | * 升级 package.json 27 | */ 28 | async function upgradePackageJson(dirPath) { 29 | const newJsonPath = path.join(templateProject, 'package.json') 30 | const oldJsonPath = path.join(dirPath, 'package.json') 31 | 32 | // eslint-disable-next-line import/no-dynamic-require 33 | const newJson = require(newJsonPath) 34 | // eslint-disable-next-line import/no-dynamic-require 35 | const oldJson = require(oldJsonPath) 36 | 37 | // 只覆盖 scripts、jest、devDependencies 三个字段 38 | oldJson.scripts = newJson.scripts 39 | oldJson.jest = newJson.jest 40 | oldJson.devDependencies = newJson.devDependencies 41 | 42 | await _.writeFile(oldJsonPath, JSON.stringify(oldJson, null, '\t')) 43 | } 44 | 45 | /** 46 | * 覆盖其他文件 47 | */ 48 | // eslint-disable-next-line complexity 49 | async function copyOthers(dirPath, options) { 50 | const override = options.override || [] 51 | let config = [] 52 | let testUtils = [] 53 | let otherTools = [] 54 | let otherTestTools = [] 55 | let otherConfig = [] 56 | let demo = [] 57 | let ignore = [] 58 | 59 | // 构建配置文件 60 | if (options.force || checkOverride('tools/config.js', override)) { 61 | config = await _.globSync('tools/config.js', globOptions) 62 | } 63 | 64 | // 测试框架工具包 65 | if (options.force || checkOverride('test/utils.js', override)) { 66 | testUtils = await _.globSync('test/utils.js', globOptions) 67 | } 68 | 69 | // 其他构建相关文件 70 | if (options.force || checkOverride('other tools files', override)) { 71 | otherTools = await _.globSync('tools/*', globOptions) 72 | otherTestTools = await _.globSync('tools/test/*', globOptions) 73 | 74 | otherTools = otherTools.filter(otherTool => otherTool.slice(-9) !== 'config.js') 75 | } 76 | 77 | // 其他构建相关配置文件 78 | if (options.force || checkOverride('other config files', override)) { 79 | const babel = await _.globSync('./.babelrc', globOptions) 80 | const eslint = await _.globSync('./.eslintrc.js', globOptions) 81 | 82 | otherConfig = [babel[0], eslint[0]] 83 | } 84 | 85 | // demo 86 | if (options.force || checkOverride('tools/demo', override)) { 87 | demo = await _.globSync('tools/demo/**/*', globOptions) 88 | } 89 | 90 | // ignore 相关配置文件 91 | if (options.force || checkOverride('ignore config', override)) { 92 | const gitignore = await _.globSync('./.gitignore', globOptions) 93 | const npmignore = await _.globSync('./.npmignore', globOptions) 94 | 95 | ignore = [gitignore[0], npmignore[0]] 96 | } 97 | 98 | // eslint-disable-next-line max-len 99 | const allFiles = [].concat(config, testUtils, otherTools, otherTestTools, otherConfig, demo, ignore) 100 | for (let i = 0, len = allFiles.length; i < len; i++) { 101 | const filePath = allFiles[i] 102 | 103 | // eslint-disable-next-line no-await-in-loop 104 | if (filePath) await _.copyFile(path.join(templateProject, filePath), path.join(dirPath, filePath)) 105 | } 106 | } 107 | 108 | /** 109 | * 执行升级命令 110 | */ 111 | async function upgrade(dirPath, options) { 112 | // 删除旧模板,拉取新模板 113 | await _.removeDir(templateProject) 114 | await _.downloadTemplate(config, options.proxy) 115 | 116 | const isTemlateExist = await _.checkDirExist(templateProject) 117 | 118 | if (!isTemlateExist) { 119 | // eslint-disable-next-line no-console 120 | console.log('can not download the template project, please check your internet connection.') 121 | process.exit(1) 122 | } 123 | 124 | const override = options.override || [] 125 | 126 | if (options.force || checkOverride('package.json', override)) { 127 | await upgradePackageJson(dirPath) 128 | } 129 | 130 | await copyOthers(dirPath, options) 131 | } 132 | 133 | module.exports = function (dirPath, options = {}) { 134 | upgrade(dirPath, options) 135 | // eslint-disable-next-line no-console 136 | .then(() => console.log(`[upgrade done]: ${dirPath}`)) 137 | // eslint-disable-next-line no-console 138 | .catch(err => console.error(err)) 139 | } 140 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const os = require('os') 4 | 5 | const glob = require('glob') 6 | const download = require('download') 7 | const rimraf = require('rimraf') 8 | const ProgressBar = require('progress') 9 | 10 | const templateDir = path.join(os.tmpdir(), './miniprogram_cli_template') 11 | 12 | /** 13 | * 异步函数封装 14 | */ 15 | function wrap(func, scope) { 16 | return function (...args) { 17 | if (args.length) { 18 | const temp = args.pop() 19 | if (typeof temp !== 'function') { 20 | args.push(temp) 21 | } 22 | } 23 | 24 | return new Promise(function (resolve, reject) { 25 | args.push(function (err, data) { 26 | if (err) reject(err) 27 | else resolve(data) 28 | }) 29 | 30 | func.apply((scope || null), args) 31 | }) 32 | } 33 | } 34 | const accessSync = wrap(fs.access) 35 | const statSync = wrap(fs.stat) 36 | const renameSync = wrap(fs.rename) 37 | const mkdirSync = wrap(fs.mkdir) 38 | const readFileSync = wrap(fs.readFile) 39 | const writeFileSync = wrap(fs.writeFile) 40 | const globSync = wrap(glob) 41 | 42 | /** 43 | * 获取模板所在目录 44 | */ 45 | function getTemplateDir() { 46 | return templateDir 47 | } 48 | 49 | /** 50 | * 递归创建目录 51 | */ 52 | async function recursiveMkdir(dirPath) { 53 | const prevDirPath = path.dirname(dirPath) 54 | try { 55 | await accessSync(prevDirPath) 56 | } catch (err) { 57 | // prevDirPath is not exist 58 | await recursiveMkdir(prevDirPath) 59 | } 60 | 61 | try { 62 | await accessSync(dirPath) 63 | 64 | const stat = await statSync(dirPath) 65 | if (stat && !stat.isDirectory()) { 66 | // dirPath already exists but is not a directory 67 | await renameSync(dirPath, `${dirPath}.bak`) // rename to a file with the suffix ending in '.bak' 68 | await mkdirSync(dirPath) 69 | } 70 | } catch (err) { 71 | // dirPath is not exist 72 | await mkdirSync(dirPath) 73 | } 74 | } 75 | 76 | /** 77 | * 拷贝文件 78 | */ 79 | async function copyFile(srcPath, distPath) { 80 | await recursiveMkdir(path.dirname(distPath)) 81 | 82 | return new Promise((resolve, reject) => { 83 | fs.createReadStream(srcPath).pipe(fs.createWriteStream(distPath)) 84 | .on('finish', () => resolve()) 85 | .on('error', err => reject(err)) 86 | }) 87 | } 88 | 89 | /** 90 | * 读取文件 91 | */ 92 | async function readFile(filePath) { 93 | try { 94 | return await readFileSync(filePath, 'utf8') 95 | } catch (err) { 96 | // eslint-disable-next-line no-console 97 | return console.error(err) 98 | } 99 | } 100 | 101 | /** 102 | * 写文件 103 | */ 104 | async function writeFile(filePath, data) { 105 | try { 106 | await recursiveMkdir(path.dirname(filePath)) 107 | return await writeFileSync(filePath, data, 'utf8') 108 | } catch (err) { 109 | // eslint-disable-next-line no-console 110 | return console.error(err) 111 | } 112 | } 113 | 114 | /** 115 | * 检查目录是否存在 116 | */ 117 | async function checkDirExist(dirPath) { 118 | try { 119 | await accessSync(dirPath) 120 | return true 121 | } catch (err) { 122 | // ignore 123 | } 124 | 125 | return false 126 | } 127 | 128 | /** 129 | * 删除目录 130 | */ 131 | async function removeDir(dirPath) { 132 | let isExist = await checkDirExist(dirPath) 133 | 134 | return new Promise((resolve, reject) => { 135 | if (!isExist) { 136 | resolve() 137 | } else { 138 | rimraf(dirPath, err => { 139 | if (err) reject(err) 140 | else resolve() 141 | }) 142 | } 143 | }) 144 | } 145 | 146 | /** 147 | * 下载模板项目 148 | */ 149 | async function downloadTemplate(config, proxy) { 150 | const templateProject = path.join(templateDir, config.name) 151 | let hasDownload = await checkDirExist(templateProject) 152 | let timer 153 | 154 | if (!hasDownload) { 155 | // mock download progress 156 | let total = 20 157 | const msg = 'now downloading template project' 158 | const bar = new ProgressBar(':bar :token1', { 159 | total, incomplete: '░', complete: '█', clear: true 160 | }) 161 | const tick = () => setTimeout(() => { 162 | total-- 163 | bar.tick({token1: msg}) 164 | 165 | if (total !== 1 && !bar.complete) tick() 166 | }, 500) 167 | const stop = () => { 168 | while (!bar.complete) bar.tick({token1: msg}) 169 | } 170 | 171 | try { 172 | timer = setTimeout(() => { 173 | // 超过一分钟没下载完,直接退出进程 174 | if (!bar.complete) { 175 | stop() 176 | // eslint-disable-next-line no-console 177 | console.log('download faild!') 178 | process.exit(1) 179 | } 180 | }, 60 * 1000) 181 | 182 | tick() // 开始 183 | 184 | await download(config.download, templateProject, { 185 | extract: true, 186 | strip: 1, 187 | mode: '666', 188 | headers: {accept: 'application/zip'}, 189 | proxy, 190 | }) 191 | 192 | stop() // 结束 193 | } catch (err) { 194 | stop() 195 | // eslint-disable-next-line no-console 196 | console.error(err) 197 | } 198 | 199 | if (timer) timer = clearTimeout(timer) 200 | } 201 | } 202 | 203 | module.exports = { 204 | recursiveMkdir, 205 | globSync, 206 | copyFile, 207 | readFile, 208 | writeFile, 209 | removeDir, 210 | getTemplateDir, 211 | checkDirExist, 212 | downloadTemplate, 213 | } 214 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path') 4 | const fs = require('fs') 5 | 6 | const program = require('commander') 7 | const inquirer = require('inquirer') 8 | 9 | const packageConfig = require('../package.json') 10 | const _ = require('./utils') 11 | const initCustomComponent = require('./custom-component/init') 12 | const upgradeCustomComponent = require('./custom-component/upgrade') 13 | const initQuickstart = require('./quickstart/init') 14 | 15 | /** 16 | * 开始初始化自定义组件 17 | */ 18 | function startInitCustomComponent(dirPath, options) { 19 | const defualtName = path.basename(dirPath) 20 | 21 | inquirer 22 | .prompt([{ 23 | type: 'input', 24 | name: 'name', 25 | message: 'please input the package name', 26 | default: defualtName, 27 | }, { 28 | type: 'input', 29 | name: 'version', 30 | message: 'please input the package version', 31 | default: '1.0.0', 32 | validate(input) { 33 | return input.match(/^\d+\.\d+\.\d+$/) ? true : 'the version must be in .. format' 34 | }, 35 | }, { 36 | type: 'input', 37 | name: 'dist', 38 | message: 'please input the miniprogram dist folder', 39 | default: 'miniprogram_dist', 40 | }, { 41 | type: 'input', 42 | name: 'git', 43 | message: 'please input the git repository url', 44 | }, { 45 | type: 'input', 46 | name: 'author', 47 | message: 'please input the author', 48 | }]) 49 | .then(answers => initCustomComponent(dirPath, Object.assign(options, answers))) 50 | // eslint-disable-next-line no-console 51 | .catch(err => console.error(err)) 52 | } 53 | 54 | /** 55 | * 开始升级自定义组件 56 | */ 57 | function startUpgradeCustomComponent(dirPath, options) { 58 | if (options.force) { 59 | upgradeCustomComponent(dirPath, options) 60 | } else { 61 | inquirer 62 | .prompt([{ 63 | type: 'checkbox', 64 | name: 'override', 65 | message: 'which files should be overrided', 66 | pageSize: 10, 67 | choices: [ 68 | {name: 'package.json (only override "scripts", "jest" and "devDependencies")', checked: true}, 69 | {name: 'tools/config.js', checked: true}, 70 | {name: 'test/utils.js', checked: true}, 71 | {name: 'other tools files (gulpfile.js, tools/build.js, tools/utils.js, tools/checkcomponents.js, tools/test/*.js)', checked: true}, 72 | {name: 'other config files (.babelrc, .eslintrc)', checked: true}, 73 | {name: 'tools/demo'}, 74 | {name: 'ignore config (.gitignore, .npmignore)'} 75 | ] 76 | }]) 77 | .then(answers => upgradeCustomComponent(dirPath, Object.assign(options, answers))) 78 | // eslint-disable-next-line no-console 79 | .catch(err => console.error(err)) 80 | } 81 | } 82 | 83 | /** 84 | * 开始初始化 85 | */ 86 | function startInit(dirPath, options) { 87 | if (options.type === 'custom-component') { 88 | // 自定义组件 89 | if (options.force) { 90 | startInitCustomComponent(dirPath, options) 91 | } else { 92 | try { 93 | fs.accessSync(path.join(dirPath, './package.json')) 94 | // eslint-disable-next-line no-console 95 | console.log(`project already exists: ${dirPath}`) 96 | } catch (err) { 97 | startInitCustomComponent(dirPath, options) 98 | } 99 | } 100 | } else { 101 | // 其他 quickstart 102 | if (options.force) { 103 | initQuickstart(dirPath, options) 104 | } else { 105 | try { 106 | fs.accessSync(path.join(dirPath, './project.config.json')) 107 | // eslint-disable-next-line no-console 108 | console.log(`project already exists: ${dirPath}`) 109 | } catch (err) { 110 | initQuickstart(dirPath, options) 111 | } 112 | } 113 | } 114 | } 115 | 116 | /** 117 | * 开始升级 118 | */ 119 | function startUpgrade(dirPath, options) { 120 | try { 121 | fs.accessSync(path.join(dirPath, './package.json')) 122 | startUpgradeCustomComponent(dirPath, options) 123 | } catch (err) { 124 | // eslint-disable-next-line no-console 125 | console.log(`project is not a valid custom component project: ${dirPath}`) 126 | } 127 | } 128 | 129 | program 130 | .version(packageConfig.version) 131 | 132 | /** 133 | * 初始化相关 134 | */ 135 | program 136 | .command('init [dirPath]') 137 | .description('create a project with template project') 138 | .option('-t, --type ', 'template project type, only accept "custom-component", "miniprogram", "plugin", "game"') 139 | .option('-f, --force', 'all files will be overrided whether it already exists or not') 140 | .option('-p, --proxy ', 'http/https request proxy') 141 | .option('-n, --newest', 'use newest template to initialize project') 142 | .action((dirPath, options) => { 143 | dirPath = dirPath || process.cwd() 144 | 145 | const choices = ['custom-component', 'miniprogram', 'plugin', 'game'] 146 | 147 | if (options.type === 'node' || options.type === 'php') { 148 | // eslint-disable-next-lint no-console 149 | console.log(`template project [ ${options.type} ] has been deprecated`) 150 | } 151 | 152 | if (!options.type || choices.indexOf(options.type) < 0) { 153 | // 未指定类型,则发起询问 154 | inquirer 155 | .prompt([{ 156 | type: 'list', 157 | name: 'type', 158 | message: 'which type of project want to use to initialize', 159 | default: 'custom-component', 160 | choices, 161 | }]) 162 | .then(answers => startInit(dirPath, Object.assign(options, answers))) 163 | // eslint-disable-next-line no-console 164 | .catch(err => console.error(err)) 165 | } else { 166 | // 已指定类型 167 | startInit(dirPath, options) 168 | } 169 | }) 170 | 171 | /** 172 | * 升级相关 173 | */ 174 | program 175 | .command('upgrade [dirPath]') 176 | .description('upgrade the miniprogram custom component framwork') 177 | .option('-f, --force', 'all files will be overrided except src folder and test case files') 178 | .option('-p, --proxy ', 'http/https request proxy') 179 | .action((dirPath, options) => { 180 | dirPath = dirPath || process.cwd() 181 | 182 | try { 183 | fs.accessSync(path.join(dirPath, './project.config.json')) 184 | 185 | inquirer 186 | .prompt([{ 187 | type: 'confirm', 188 | name: 'force', 189 | message: 'this project doesn\'t look like a custom component project, is it stop upgrading?', 190 | default: true, 191 | }]) 192 | .then(answers => { 193 | if (!answers.force) { 194 | // 猜测为非自定义组件项目,仍旧强制升级 195 | startUpgrade(dirPath, options) 196 | } 197 | }) 198 | // eslint-disable-next-line no-console 199 | .catch(err => console.error(err)) 200 | } catch (err) { 201 | // ignore 202 | startUpgrade(dirPath, options) 203 | } 204 | }) 205 | 206 | /** 207 | * 缓存相关 208 | */ 209 | program 210 | .command('cache') 211 | .description('show the path of template projects cache') 212 | .option('-c, --clear', 'clear cache') 213 | .action(options => { 214 | const templateDir = _.getTemplateDir() 215 | 216 | if (options.clear) { 217 | _.removeDir(templateDir) 218 | // eslint-disable-next-line no-console 219 | .then(() => console.log(`[remove cache done]: ${templateDir}`)) 220 | // eslint-disable-next-line no-console 221 | .catch(err => console.error(err)) 222 | } else { 223 | // eslint-disable-next-line no-console 224 | console.log(templateDir) 225 | } 226 | }) 227 | 228 | program.parse(process.argv) 229 | --------------------------------------------------------------------------------