@ygyg/yg-cli
Version:
A simple CLI for front-end engineering automation construction tool.
668 lines (604 loc) • 19.9 kB
JavaScript
// 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];
}
};