UNPKG

@ygyg/yg-cli

Version:

A simple CLI for front-end engineering automation construction tool.

668 lines (604 loc) 19.9 kB
// const which = require('which'); const fs = require('fs'); const path = require('path'); const childProcess = require('child_process'); const existsSync = require('fs').existsSync; // node自带的fs模块下的existsSync方法,用于检测路径是否存在。(会阻塞) const chalk = require('chalk'); // 用于高亮终端打印出的信息(命令行字体颜色) const which = require('npm-which')(__dirname); const lodash = require('lodash'); const util = require('util'); const filelist = require('./file-list'); const { rcFile } = require('rc-config-loader'); const https = require('https'); const http = require('http'); const axios = require('axios'); const execSync = require('child_process').execSync; // 同步子进程 const { join, isAbsolute } = path; exports.logStep = function(name) { // console.log(`${chalk.green('[ygd-scripts]')} ${chalk.magenta.bold(name)}`); console.log('[ygd-scripts]', name); }; exports.printErrorAndExit = function(message) { console.error(chalk.red(message)); process.exit(1); }; exports.Install = function() { var npm = this.findNpm(); this.runCmd(npm, ['install'], () => { this.logStep(npm + ' install end'); }); }; // 查找系统中用于安装依赖包的命令 exports.findNpm = function() { var npms = ['cnpm', 'npm']; for (var i = 0; i < npms.length; i++) { try { // 查找环境变量下指定的可执行文件的第一个实例 which.sync(npms[i]); this.logStep('use npm: ' + npms[i]); return npms[i]; } catch (e) { console.warn(e); } } throw new Error(chalk.red('please install npm')); }; /** * 构建多渠道 * @param {*} params */ exports.runSaaSBuild = async function(params) { this.logStep(`runSaaSBuild: ${JSON.stringify(params)}`); const { channelName, channelServer } = params; const currDomain = await this.parseDomain(params); this.logStep(`currDomain : ${currDomain}`); process.env.YG_DOMAIN = currDomain; process.env.YG_CHANNEL = channelName; process.env.UMI_ENV = 'prod'; process.env.NODE_ENV = 'production'; process.env.YG_ENV = channelServer; this.runCmd('umi', ['build'], function() { console.log('umi:%j build end', channelName); }); }; exports.parseDomain = async function(params) { const { channelName, channelServer } = params; if (channelServer !== undefined && lodash.startsWith(channelServer, 'http')) { // TODO: 由于新规则httpUrls根据defaultSetting的参数读取,此处先不处理跳转逻辑 this.logStep(`http-domain=${channelServer}`); return channelServer; } if (channelServer === undefined) { this.printErrorAndExit('未指定环境'); } // 读取rc配置 const rcApiPrefix = await this.getRcKey('apiprefix'); if (!!rcApiPrefix) { if (lodash.indexOf(rcApiPrefix.envs, channelServer) !== -1) { // ygego 单独处理www const tempChannelName = channelName === 'ygego' ? 'www' : channelName; if (channelServer === 'prod') { return 'https://{channelName}.ygyg.cn'.replace( '{channelName}', tempChannelName, ); } return rcApiPrefix.url .replace('{channelName}', tempChannelName) .replace('{channelServer}', channelServer); } } return channelName === 'ygego' ? `http://www.ygego.${channelServer}` : `http://${channelName}.ygego.${channelServer}`; }; exports.runSaaSDev = async function(params) { this.logStep(`runSaaSDev: ${JSON.stringify(params)}`); const { channelName, channelServer } = params; const currDomain = await this.parseDomain(params); this.logStep(`currDomain : ${currDomain}`); process.env.YG_DOMAIN = currDomain; process.env.YG_CHANNEL = channelName; process.env.YG_ENV = channelServer; // process.env.env=channelName; // process.env.PORT = 8701; process.env.NODE_ENV = 'development'; // const remotePath = '/Users/fugang/workspace/xinao/channel-desk'; // filelist.getFiles(remotePath); const rcCmd = await this.getRcKey('commands'); const { dev } = rcCmd; if (dev === undefined) { this.printErrorAndExit('.ygclirc.js commands.dev not found'); } if (dev === 'umi') { this.runCmd('umi', ['dev'], () => { this.logStep(`umi:${channelName} dev end`); // console.log('umi:%j build end', channelName); }); } else { this.runCmd( 'egg-bin', [ 'dev', `--port=${process.env.PORT}`, `--YG_ENV=${channelServer}`, `--YG_DOMAIN=${currDomain}`, `--env=${channelName}`, ], () => { this.logStep(`umi:${channelName} dev end`); }, ); } // PORT=8701 NODE_ENV=development egg-bin dev --sticky // --YG_ENV=alpha --env=ygego --port=8701 }; exports.runStartLocal = async function(params) { const { channelName, channelServer } = params; process.env.NODE_ENV = 'production'; process.env.YG_CHANNEL = channelName; process.env.YG_ENV = channelServer; // "start": "cross-env egg-scripts start --daemon --title=portal-saas-ssr", // "debug": "cross-env RM_TMPDIR=none COMPRESS=none egg-bin debug", await this.runCmd( 'egg-scripts', [ 'start', '--daemon', `--title=${process.env.TITLE}`, `--YG_ENV=${channelServer}`, `--port=${process.env.PORT}`, ], () => { this.logStep(`yg egg local start: ${process.env.NODE_ENV} dev ok`); }, ); }; exports.runStart = async function() { process.env.NODE_ENV = 'production'; // "start": "cross-env egg-scripts start --daemon --title=portal-saas-ssr", // "debug": "cross-env RM_TMPDIR=none COMPRESS=none egg-bin debug", await this.runCmd( 'egg-scripts', [ 'start', '--daemon', `--title=${process.env.TITLE}`, `--port=${process.env.PORT}`, ], () => { this.logStep(`yg egg deploy start: ${process.env.NODE_ENV} dev ok`); }, ); }; exports.runStop = async function(fn) { // "start": "cross-env egg-scripts start --daemon --title=portal-saas-ssr", // "debug": "cross-env RM_TMPDIR=none COMPRESS=none egg-bin debug", await this.runCmd( 'egg-scripts', ['stop', `--title=${process.env.TITLE}`], () => { if (fn) { fn(); } this.logStep(`yg egg stop: ${process.env.NODE_ENV} dev ok`); }, ); }; exports.runReStart = async function() { // "start": "cross-env egg-scripts start --daemon --title=portal-saas-ssr", // "debug": "cross-env RM_TMPDIR=none COMPRESS=none egg-bin debug", await this.runStop(() => { this.runStart(); }); }; // 开启子进程来执行npm install命令 exports.runCmd = async function(cmdName, args, fn) { args = args || []; // How to sync node_modules with actual package.json? // If your new branch has new npm packages or updated version dependencies, just run $ npm install again after switching branches. const cmd = await which.sync(cmdName, { nothrow: true }); this.logStep(cmd, '----cmd'); if (!cmd) { this.logStep(`not found: ${cmdName}`); return; } var runner = childProcess.spawn(cmd, args, { stdio: 'inherit', env: process.env, }); await runner.on('close', function(code) { if (fn) { fn(code); } }); }; exports.parseArg = function(opt) { const { channel, server, channelserver } = opt; const f = channel !== undefined || server !== undefined; const arr_channel = !!channelserver && channelserver.indexOf(':') !== -1 ? channelserver.split(':') : []; const channelName = f ? channel : arr_channel[0]; let channelServer = f ? server : arr_channel[1]; if (!!channelServer && channelServer.indexOf('http') !== -1) { channelServer = channelserver.substring(channelName.length + 1); } this.logStep('当前正在准备编译环境...'); if (channelName !== undefined && channelServer !== undefined) { return { channelName, channelServer }; } if (channelServer) { return { channelServer }; } return undefined; }; /** * 加载yg-cli 配置文件 * @param {*} rcFileName */ exports.loadRcFile = async function(rcFileName) { var that = this; try { const pkg = await this.getPkg(process.cwd()); const results = await rcFile(rcFileName); if (!results && !pkg.ygcliConfig) { // that.printErrorAndExit('.ygclirc.js not found'); console.error(chalk.red('.ygclirc.js not found')); return {}; } if (results) { return results.config; } if (pkg.ygcliConfig) { return pkg.ygcliConfig; } } catch (error) { // Found it, but it is parsing error that.printErrorAndExit('.ygclirc.js not found'); } }; exports.getRcKey = async function(key) { const cliRc = await this.loadRcFile('ygcli'); return cliRc[key]; }; /** * 按指定环境 通常由build与dev 指定 * 更新代码中的配置变量 * build 会分为多saas渠道 * @param {*} opt */ exports.updateSettings = async function(opt) { await this.writeJson(opt); }; /** * 校验输入参数 * @param {*} params */ exports.checkArgs = async function(params) { if (!params) { return; } const { channelName, channelServer } = params; this.logStep(`channelName : ${channelName}`); this.logStep(`serverName : ${channelServer}`); // return; if (!channelName && !channelServer) { this.printErrorAndExit('please input channelName and serverName'); } }; exports.isFileSync = async function(aPath) { try { const res = await fs.statSync(aPath).isFile(); return res; } catch (e) { if (e.code === 'ENOENT') { return false; } throw e; } }; exports.updateSettingUrls = async function(params) { if (!params) { this.logStep('没有配置环境变量'); return; } this.logStep('正在根据配置文件生成httpUrls'); const { channelServer } = params; // const envs = await this.getEnvs(); const channels = await this.getChannels('all'); const exConfig = await this.getExConfig(); const staticConfig = await this.getStaicConfig(); const urlsConfig = await this.getUrlsConfig(); const exEnvs = Array.isArray(exConfig.envs) ? exConfig.envs : []; const staticEnvs = Array.isArray(staticConfig.envs) ? staticConfig.envs : []; // for (let i = 0; i < envs.length; i += 1) { const curr_env = channelServer; // 'prod'; const httpProtocol = curr_env !== 'prod' ? 'http' : 'https'; const httpDomain = curr_env !== 'prod' ? `ygego.${curr_env}` : 'ygyg.cn'; const thirdArr = lodash.keys(urlsConfig); const httpUrls = { [curr_env]: {}, }; for (let i = 0; i < thirdArr.length; i += 1) { const currKey = thirdArr[i]; httpUrls[curr_env][currKey] = urlsConfig[currKey] .replace('http', httpProtocol) .replace('ygego.{channelServer}', httpDomain) .replace('{channelServer}', curr_env); } httpUrls[curr_env].exYgego = !exEnvs.includes(curr_env) ? 'http://ex.ygego.beta2' : `${httpProtocol}://ex.${httpDomain}`; httpUrls[curr_env].static = !staticEnvs.includes(curr_env) ? 'http://static.ygego.alpha' : `${httpProtocol}://static.${httpDomain}`; this.logStep(`setHttpUrls : ${JSON.stringify(httpUrls)}`); await this.writeJson({ channels: channels, httpUrls: httpUrls }); }; exports.writeJson = async function(params) { const { path: someFile } = await this.getRcKey('envfile'); if (someFile === undefined) { this.printErrorAndExit('.ygclirc.js envfile.path not found'); } // 现将json文件读出来 const existFile = await this.isFileSync(someFile); if (!existFile) { const appendFile = util.promisify(fs.appendFile); await appendFile(someFile, '{}', 'utf8'); this.logStep(`${someFile}文件创建成功`); } const readFile = util.promisify(fs.readFile); const writeFile = util.promisify(fs.writeFile); // let setting = await readFile(someFile); this.logStep('setting : 准备写入文件'); let setting = await readFile(someFile).then((data) => data.toString() === '' ? {} : JSON.parse(data.toString()), ); // console.log(JSON.parse(setting),'setting'); if (setting) { lodash.forIn(params, (value, key) => { if (key !== undefined && value !== undefined) { setting[key] = value; // 重新赋值 this.logStep(`success write setting : ${key}`); } }); const result = JSON.stringify(setting); await writeFile(someFile, result); } }; /** * all 为channels整个对象 * key为 channels对应的key * @param {*} type */ exports.getChannels = async function(type = 'key') { const rcChannels = await this.getRcKey('channels'); if (type === 'all') { return rcChannels; } if (!rcChannels) { return ['www', 'ygego', 'aode', 'changsha', 'zyzt', 'slgf', 'czyg', 'xdhb']; } return lodash.map(rcChannels, (item, key) => key); }; exports.getFirstChannel = async function() { const channel_cfg = await this.getChannels('all'); const defaultChannel = lodash.get(channel_cfg.default, 'name', ''); return defaultChannel; }; /** * 验证交互参数,处理其他项目默认值 * @param {*} ansParams */ exports.parseAnswer = async function(ansParams) { const { server, channelserver } = ansParams; const needChannel = await this.needChannel(); // 默认为true if (!needChannel && !channelserver) { const myChannel = await this.getFirstChannel(); const myParams = this.parseArg({ channel: myChannel, server }); // console.log(myParams, myChannel, 111); return myParams; } return this.parseArg(ansParams); }; exports.getEnvs = async function() { const rcApiPrefix = await this.getRcKey('apiprefix'); if (!rcApiPrefix) { return [ 'alpha1', 'alpha2', 'alpha3', 'alpha4', 'alpha5', 'test1', 'test2', 'test3', 'test4', 'test5', 'test5', 'test6', 'beta', 'beta1', 'beta2', 'beta3', 'prod', ]; } return rcApiPrefix.envs; }; // 增加ex的配置 exports.getExConfig = async function() { const rcApiPrefix = await this.getRcKey('apiprefix'); if (!rcApiPrefix) { return { envs: ['test7', 'beta', 'prod'], }; } return rcApiPrefix.exeption; }; // 增加ex的配置 exports.getStaicConfig = async function() { const rcApiPrefix = await this.getRcKey('apiprefix'); if (!rcApiPrefix) { return { envs: ['beta', 'prod'], }; } if (!rcApiPrefix.static) { return { envs: ['beta', 'prod'], }; } return rcApiPrefix.static; }; // 增加ex的配置 exports.getUrlsConfig = async function() { const rcApiPrefix = await this.getRcKey('apiprefix'); const defaultUrls = { projectYgego: 'http://project.ygego.{channelServer}', platformDesk: 'http://yun.ygego.{channelServer}', businessWorkbench: 'http://wx.ygego.{channelServer}', channelDesk: 'http://www.ygego.{channelServer}', httpUrl: '{channelServer}', channelBackstage: 'http://admin.ygego.{channelServer}', exYgego: 'http://ex.ygego.{channelServer}', webMallOp: 'http://csadmin.ygego.{channelServer}', mallChannelDesk: 'http://cs.ygego.{channelServer}', inventory: 'http://inventory.ygego.{channelServer}', platformBackstage: 'http://yunadmin.ygego.{channelServer}', shopYgego: 'http://shop.ygego.{channelServer}', ssoRedirect: 'https://signin.ygego.{channelServer}', ygpc: 'https://ygpc.ygego.{channelServer}', }; if (!rcApiPrefix) { return defaultUrls; } return rcApiPrefix.thirdPartyUrls || defaultUrls; }; exports.needChannel = async function() { const rcNeedChannel = await this.getRcKey('needChannel'); if (rcNeedChannel === undefined) { return true; } if (!rcNeedChannel) { // false return rcNeedChannel; } return true; }; exports.getLintPath = async function(params = {}) { const lints = await this.getRcKey('lints'); let { lintpath = './src', group = {} } = lints || {}; const { module_key } = params; if (group[module_key]) { return group[module_key]; } return lintpath; }; exports.getLintGroup = async function(params = {}) { const { group } = await this.getRcKey('lints'); if (group === undefined) { this.printErrorAndExit('.ygclirc.js group not found'); } return group; }; exports.getCwd = async function() { let cwd = process.cwd(); if (process.env.APP_ROOT) { // avoid repeat cwd path if (!isAbsolute(process.env.APP_ROOT)) { return join(cwd, process.env.APP_ROOT); } return process.env.APP_ROOT; } return cwd; }; exports.getPkg = async function(dir) { try { return require(join(this.getCwd(), 'package.json')); } catch (error) { try { return require(join(dir, 'package.json')); } catch (error) { return null; } } }; exports.getCommits = async function () { let gitlabUrl = process.env.GITLAB_URL || 'https://git.ennew.alpha/'; if (gitlabUrl.endsWith('/')) {gitlabUrl = gitlabUrl.substr(0, gitlabUrl.length - 1);} const commit = execSync('git show -s --format=%H').toString() .trim(); // 当前提交的版本号 let name = execSync('git show -s --format=%cn').toString() .trim(); // 姓名 let email = execSync('git show -s --format=%ce').toString() .trim(); // 邮箱 let date = new Date(execSync('git show -s --format=%cd').toString()); // 日期 let message = execSync('git show -s --format=%s').toString() .trim(); // 说明 const branch = execSync('git rev-parse --abbrev-ref HEAD').toString() .replace(/\s+/, ''); const repositoryUrl = execSync('git remote get-url --push origin').toString() .trim(); const projectUrl = repositoryUrl.replace('.git', ''); let projectName = projectUrl.replace(gitlabUrl, ''); if (projectName.startsWith('/')) {projectName = projectName.substr(1);} return {commit, name, email, date, message, branch, projectName, repositoryUrl, projectUrl, gitlabUrl}; }; // TODO: lerna 子包的部署father-build,需检查../../ 即package同级 exports.isLernaPackage = function (root) { return existsSync(join(process.cwd(), 'lerna.json')) || existsSync(join(process.cwd(), '../../lerna.json')); }; exports.checkScripts = function() { this.logStep('检查script脚本'); const pkg = this.getPkg(); const dependScripts = ['lint', 'test', 'build', 'release', 'site', 'test:coverage']; const needScripts = []; for (let s of dependScripts) { if (!pkg.scripts[s]) { needScripts.push(s); } } if (needScripts.length > 0) { this.logStep('发现package.json的script脚本未包含流水线脚本,请联系管理员'); this.logStep(`流水线脚本:${needScripts.join(',')}`); } }; exports.checkCommit = function() { const msgPath = process.env.HUSKY_GIT_PARAMS; const msg = fs.readFileSync(msgPath, 'utf-8').trim(); const commitRE = /^(feat|fix|docs|chore|style|refactor|perf|test|build|ci|revert|merge)(\(.+\))?: .{1,50}/; if (!commitRE.test(msg)) { console.error( ` ${chalk.bgRed.white(' ERROR ')} ${chalk.red('invalid commit message format.')}\n\n${chalk.red( ' Proper commit message format is required for automated changelog generation. Examples:\n\n', )}`, ); process.exit(1); } }; exports.checkDependPackages = function(dependPackages) { this.logStep('检查script脚本'); const pkg = this.getPkg(); // const dependScripts = ['lint', 'test', 'build', 'release', 'site', 'test:coverage']; const needScripts = []; for (let s of dependPackages) { if (!pkg.scripts[s]) { needScripts.push(s); } } if (needScripts.length > 0) { this.logStep('发现package.json的script脚本未包含流水线脚本,请联系管理员'); this.logStep(`流水线脚本:${needScripts.join(',')}`); } }; exports.getPkgRegistry = async function (url) { try { const res = await axios.get(url, {}, {timeout: 6000}); return [null, res.data]; } catch (error) { return [error, null]; } };