执行命令

​utils\utils\lib\index.js​​ 模块下新建两个方法

/**
* @description: 封装一个 spawn 方法,兼容 mac 和 windows
* windows : cp.spawn('cmd',['/c','node','-e',code],{})
* mac : cp.spawn('node', ['-e', code],{})
* @param {*} command 'cmd'
* @param {*} args ['/c','node','-e',code]
* @param {*} options {}
* @return {*} cp.spawn(cmd, cmdArgs, options)
*/
function spawn(command, args, options = {}) {
const cp = require('child_process');
const win32 = process.platform === 'win32';
const cmd = win32 ? 'cmd' : command;
const cmdArgs = win32 ? ['/c'].concat(command, args) : args;
return cp.spawn(cmd, cmdArgs, options);
}

/**
* @description: 异步执行命令
* @param {*} 参数同上
* @return {*}
*/
function execAsync(command, args, options = {}) {
return new Promise((resolve, reject) => {
const p = spawn(command, args, options)
p.on('error', e => {
reject(e)
})
p.on('exit', c => {
resolve(c)
})
})
}

module.exports = {
spawn,
execAsync
};

模板安装


​commands\init\lib\index.js​


在命令的执行阶段添加安装模板的方法。

/**
* @description: 命令的执行阶段
* @param {*}
* @return {*}
*/
async exec() {
try {
// 1.准备阶段
const projectInfo = await this.prepare();
if (projectInfo) {
this.projectInfo = projectInfo;
log.verbose('???????? ~ 项目详情', projectInfo);
// 2.下载模板
await this.downloadTemplate()
// 3.安装模板
await this.installTemplate()
}
} catch (error) {
log.error(error.message);
}
}

安装模板,根据模板类型安装标准模板或者自定义模板。

const TEMPLATE_TYPE_NORMAL = 'normal';
const TEMPLATE_TYPE_CUSTOM = 'custom';
/**
* @description: 安装模板
* @param {*}
* @return {*}
*/
async installTemplate() {
// 安装标准模板
if (this.templateInfo.type === TEMPLATE_TYPE_NORMAL) {
await this.installNormalTemplate()
}
// 安装自定义模板
else if (this.templateInfo.type === TEMPLATE_TYPE_CUSTOM) {
await this.installCustomTemplate()
}
// 位置类型
else {
throw new Error("无法解析的项目类型")
}
}

封装的执行命令的方法。

/**
* @description: 执行命令
* @param {*} cmd 命令
* @param {*} message 执行后提示的消息
* @return {*}
*/
async execCommand(cmdInfo, message) {
if (cmdInfo) {
// npm install ==> [npm,install]
const installCmd = cmdInfo.split(' ')
// npm
const cmd = checkCommand(installCmd[0])
if (!cmd) {
throw new Error("命令不存在")
}
// [install]
const args = installCmd.slice(1)
const installResult = await execAsync(cmd, args, {
stdio: 'inherit',
cwd: process.cwd()
})
if (installResult === 0) {
log.warn(message)
}
}
}

安装标准模板。

/**
* @description: 安装标准模板
* @param {*}
* @return {*}
*/
async installNormalTemplate() {
const spinner = spinnerStart('模板安装中,请稍候...')
await sleep()
try {
// 获取模板缓存路径
// console.log('✅✅✅ ~ ', this.templateNpm);
// 获取模板所在目录
const templatePath = path.resolve(this.templateNpm.cacheFilePath, 'template')
// 获取当前目录
const targetPath = process.cwd()
// 确保目录存在 不存在会创建
fse.ensureDirSync(templatePath);
fse.ensureDirSync(targetPath);
// 拷贝模板到当前目录
fse.copySync(templatePath, targetPath)
} catch (error) {
log.error(error.message)
} finally {
spinner.stop(true)
log.warn('模板安装成功')
}
// ejs 模板渲染
const ignore = ['node_modules/**', 'public/**']
const ops = {
ignore
}
await this.ejsRender(ops)
const { installCommand, startCommand } = this.templateInfo
// 依赖安装
await this.execCommand(installCommand, '依赖安装成功')
// 启动命令
await this.execCommand(startCommand, '项目启动成功')
}

安装自定义模板,暂时没有开发到这里。

/**
* @description: 安装自定义模板
* @param {*}
* @return {*}
*/
async installCustomTemplate() {
console.log('✅✅✅ ~ 安装自定义模板');
}

检查命令,防止产生意外情况,命令必须是以下几种之一才会执行。

const WHITE_COMMAND = ['npm', 'cnpm', 'yarn']

/**
* @description: 检查命令是否在白名单中
* @param {*} cmd
* @return {*}
*/
function checkCommand(cmd) {
if (WHITE_COMMAND.includes(cmd)) {
return cmd
}
return null
}

修改模板代码

使用 ​​ejs​​​ 修改 ​​hzw-cli-dev-template-vue3​​​ 这个模板 ​​template​​​ 目录下的 ​​package.json​​ 文件

"name": "<%= className %>",
"version": "<%= version %>",

然后修改外层 ​​package.json​​​ 的版本号 ​​npm publish​​​ 发布新版本到 ​​npm​​。

PS: 因为发布到 ​​npm​​​ 的版本号必须是正常的版本号,所以才需要嵌套一层,将外层作为 ​​npm​​ 模块,内层作为模板。


​commands\init\lib\index.js​


需要在 ​​getInfo​​​ 这个方法中将 ​​projectName​​​ 转换为链接线格式 ​​project-name​

if (info.project) {
// kebab-case 这个库如果是大写字母开头会多出一个 - , 所以使用 replace 把第一个 - 给去掉
info.project = require('kebab-case')(info.project).replace(/^-/, '')
}

使用 ejs 进行模板渲染

安装 ​​ejs​​​ 和 ​​glob​

lerna add ejs commands/init
lerna add glob commands/init


​commands\init\lib\index.js​


/**
* @description: ejs 模板渲染
* @param {*}
* @return {*}
*/
ejsRender(options) {
return new Promise((resolve, reject) => {
// 遍历文件列表
glob("**", {
cwd: process.cwd(),
nodir: true,
ignore: options.ignore || []
}, (err, files) => {
if (err) {
reject(err)
}
// 对文件列表使用 ejs 进行渲染
Promise.all(files.map((file) => {
const filePath = path.join(process.cwd(), file)
return new Promise((resolve1, reject1) => {
ejs.renderFile(filePath, this.projectInfo, {}, (err, res) => {
if (err) {
reject1(err)
}
// 将源文件替换成 ejs 渲染后的文件
fse.writeFileSync(filePath, res)
resolve1(res)
})
})
}))
.then(() => resolve(files))
.catch((err) => reject(err))
})
})
}

init 命令直接传入项目名称功能支持

/**
* @description: 选择创建项目或者组件 获取项目的基本信息 return Object
* @param {*}
* @return {*} 项目的基本信息
*/
async getInfo() {
let info = {};
// 选择创建项目或者组件;
const {
type
} = await inquirer.prompt({
type: 'list',
message: '请选择初始化类型',
name: 'type',
default: TYPE_PROJECT,
choices: [{
name: '项目',
value: TYPE_PROJECT,
},
{
name: '组件',
value: TYPE_COMPONENT,
},
],
});
log.verbose('type', type);
// 获取项目的基本信息;
if (type === TYPE_COMPONENT) { }
const isValidateName = (a) => {
const reg =
/^[a-zA-Z]+([-][a-zA-Z0-9]|[_][a-zA-Z0-9]|[a-zA-Z0-9])*$/;
return reg.test(a)
}
// 是否在执行init 命令的时候就传入了正确的项目名称
const isTrueName = isValidateName(this.projectName)
console.log('???????? ~ InitCommand ~ this.projectName', this.projectName);
console.log('???????? ~ InitCommand ~ isTrueName', isTrueName);

const promptArr = [{
type: 'input',
message: '请输入项目版本号',
name: 'version',
default: '1.0.0',
validate: (a) => {
return !!semver.valid(a) || '请输入合法的版本号';
},
filter: (a) => {
if (!!semver.valid(a)) {
return semver.valid(a);
}
return a;
},
},
{
type: 'list',
message: '请选择项目模板',
name: 'template',
default: 'vue3',
choices: this.createTemplateChoices()
},]
if (type === TYPE_PROJECT) {
const projectPrompt = {
type: 'input',
message: '请输入项目名称',
name: 'project',
validate: (a) => {
if (isValidateName(a)) {
return true;
}
return '要求英文字母开头,数字或字母结尾,字符只允许使用 - 以及 _ ';
},
}
if (isTrueName) {
info.project = this.projectName
} else {
promptArr.unshift(projectPrompt)
}
const answers = await inquirer.prompt(promptArr);
info = {
...info,
...answers,
type,
};
}

// 将项目名称改成连接线形式
if (info.project) {
// kebab-case 这个库如果是大写字母开头会多出一个 - , 所以使用 replace 把第一个 - 给去掉
info.className = require('kebab-case')(info.project).replace(/^-/, '')
}
return info;
}