Open AurorePaladin opened 2 years ago
commander
#!/usr/bin/env node
const { Command } = require('commander');
const pkg = require('../package.json');
// 第一种使用方法: 获取commande的单例
// const { program } = commander;
// 第二种使用方法: 手动实例化一个commander实例
const program = new Command();
// 注册参数
program
.name(Object.keys(pkg.bin)[0]) // 包名
.usage('<command> [options]') // 使用建议
.version(pkg.version) // 获取版本号
.option('-d, --debug', '是否开启调试模式', false) // 配置参数
.option('-e, --envName <envName>', '获取环境变量名称', '.imooc-test') // 获取环境变量
// console.log(program.envName); // 获取输入的环境变量参数
// program.outputHelp(); // 输出帮助信息
// console.log(program.opts()); // 输出所有注册的参数 { version: '1.0.3', debug: false, envName: '123' }
// command 注册的是当前脚手架下的命令且返回值是command对象而不是program对象
const clone = program.command('clone <source> [destination]'); // 注册命令名称
clone
.description('clone a repository into a newly created directory')
.option('-f, --force', '是否强制克隆')
.action((source, destination, cmdObj) => {
console.log(source, destination, cmdObj.force);
}); // 注册clone命令的回调
// addCommand 注册的是当前脚手架下的子命令
const service = new Command('service');
service
.command('start [port]')
.description('start service at some port')
.action((prot) => {
console.log('do service start', prot)
});
service
.command('stop [port]')
.description('stop service')
.action((prot) => {
console.log('stop service')
});
program.addCommand(service);
program
.command('install [name]', 'install one or more package', { // 这条命令执行的是个脚本文件相当于在当前目录下执行node node_module@winbridge-cli/core/bin
executableFile: 'node_modules/@winbridge-cli/core/bin', // 设置可执行文件路径
// isDefault: true, // 执行imooc-test的时候默认执行这条命令
// hidden: true // 隐藏imooc-test -h 中command的隐藏
})
.alias('i');
// 命令注册的自动匹配
// program
// .arguments('<cmd> [options]')
// .description('test command', {
// cmd: 'command to run',
// options: 'options for command'
// })
// .action((cmd, options) => {
// console.log(cmd, options)
// })
// 高级定制1: 自定义help信息 program.helpInformation() 获取帮助信息
// console.log(program.helpInformation());
// 方法一
// program.helpInformation = function() { return '' } // 定制 imooc-test --help 返回的帮助信息
// 方法二
// program.on('--help', function() { // 监听 命令行输入的 --help 参数, 并返回信息
// console.log('your help information');
// })
// 高级定制2: 实现 dubug 模式
program.on('option:debug', function() {
if(program.debug) {
process.env.LOG_LEVEL = 'verbose';
}
console.log(process.env.LOG_LEVEL);
})
// 高级定制3: 对未知命令监听
program.on('command:*', function(obj) {
console.error('未知的命令:' + obj[0]);
const availableCommands = program.commands.map(cmd => cmd.name());
console.log('可用命令: '+ availableCommands.join(', '));
});
program
.parse(process.argv); // 参数解析
在bin文件中建立主流程并修改package里面的入口文件 由于是入口文件需要node解析所以要在头部加上#! /usr/bin/env node
if(importLocal(__filename)){ require('npmlog').info('cli','正在使用 cli本地版本') }else{ require('../lib')(process.argv.slice(2)) }
主要来监控不是用本地本地版本(参考了lerna的代码)
是一个入口文件
### 三、prepare 具体代码
- checkPkgVersion 检查版本号
其中涉及到了require的用法,require可以引入js文件和json文件,默认除了json都默认js文件。
- checkNodeVersion 检查node版本
设置一个最低版本比较当前版本和最低版本,如果版本吧太低要处理,会用到semver组件来比较版本大小
- checkRoot 检查是否为root权限,如果不是升级问root权限,会用到root-check组件
- checkUserHome 检查用户主目录 会用到osHomedir组件
- checkArgs 检查入参 监听入参然后处理 会用到minimist组件来处理参数
- checkEnv 检查环境变量 判断是否有特定的环境变量如果有就用dotenv组件解析内容,将其分配给 process.env,如果没有就手动添加
- checkGlobalUpdata 检查版本提示更新 要获取npm上最新版本,比较和现在的版本,如果有差别要提示更新 会新建一个npm组件来获取这个事。如下:
- 要要npm给的接口,可以选择速度快的淘宝镜像'http://registry.npmjs.org' : 'http://registry.npm.taobao.org'加上npmName,要用到axios组件,注意返回promise对象
- 对获取的版本号进行排序,拿最新的和现有版本比较
## commander 的使用方法
- 用途是注册命令和yargs的用途类似
- 阅读commander npm的官方文档
## 用node支持ES model模式的开发
- node现在是不支持ES model开发的,只能用CMD模式开发即require来开发,如果要用ES model开发有两个方法,一个是用webpack来把代码编译成node能识别的模式,一种是用.mjs来作文js代码的后缀,还在试验环节,目前主要学习第一种方法
### 使用webpack
下载webpack和webpack-cli
,新建webpack.config.js文件
```javascript
const path = require('path')
module.exports = {
entry:'./bin/core.js',
output:{
path: path.join(__dirname,'/dist'),
filename:'core.js'
},
mode:'development',
target:'node',
}
其中entry设置传入的其实文件;output设置传出的文件;mode是文件类型有development和production两种开发模式易于阅读,生产模式体积小;target设置node可以用node环境里面的内置库 在scripts里面添加"build": "webpack","dev": "webpack -w"命令 使用npm run build和npm run dev来执行webpack build是一次性编译,dev是持续编译终端要被占用
try {
checkPkgVersion()
checkNodeVersion()
checkRoot()
checkUserHome()
checkInputArgs()
checkEnv()
await checkGlobalUpdate()
} catch(error) {
log.error(error.message)
}
// 检查是否需要全局更新
async function checkGlobalUpdate() {
const currentVersion = pkg.version
const npmName = pkg.name
const lastVersions = await getNpmSemverVersions(npmName, currentVersion)
if(lastVersions && semver.gt(lastVersions, currentVersion)) {
log.warn('更新提示', colors.yellow(
dedent`
请更新 ${npmName}
当前版本: ${currentVersion}
最新版本: ${lastVersions}
更新命令: npm install -g ${npmName}@${lastVersions}
`))
}
}
// 环境变量检查
function checkEnv() {
const dotenv = require('dotenv')
const dotenvPath = path.resolve(userHome, '.env')
if(pathExists(dotenvPath)) {
config = dotenv.config({
path: dotenvPath
})
}
createDefaultConfig()
log.verbose('环境变量:', process.env.CLI_HOME_PATH)
}
// 创建默认的环境变量配置
function createDefaultConfig() {
const cliConfig = {
home: userHome
}
process.env.CLI_HOME_PATH = cliConfig['cliHome'] = process.env.CLI_HOME
? path.join(userHome, process.env.CLI_HOME)
: path.join(userHome, constant.DEFAULT_CLI_HOME)
}
// 入参检查
function checkInputArgs() {
args = require('minimist')(process.argv.slice(2))
checkArgs(args)
}
// debug 模式判断
function checkArgs(args) {
log.level = process.env.LOG_LEVEL = args.debug
? 'verbose'
: 'info'
}
// 检查 用户主目录
function checkUserHome() {
if(!userHome || !pathExists(userHome)) {
throw new Error(colors.red('当前登陆用户主目录不存在'))
}
}
// 检查登陆帐号的级别 以及 降级
function checkRoot() {
require('root-check')()
}
// 检查 node 的版本
function checkNodeVersion() {
const currentNodeVersion = process.version
const lowestNodeVersion = constant.LOWEST_NODE_VERSION
if(!semver.gte(currentNodeVersion, lowestNodeVersion)) {
throw new Error(colors.red(`weilai-cli 需要安装 v${lowestNodeVersion} 以上版本的 Node.js`))
}
log.notice('node', process.version)
}
// 检查 package 的版本
function checkPkgVersion() {
log.notice('cli', pkg.version)
}
#! /usr/bin/env node
const commander = require('commander')
const pkg = require('../package.json')
// 获取 commander 的单例
// const { program } = commander
// 手动实例化一个 commander 实例
const program = new commander.Command()
program
.name(Object.keys(pkg.bin)[0])
.usage('<command> [options]')
.version(pkg.version)
.option('-d, --debug', '是否开启调试模式', false)
.option('-e, --env <envName>', '获取环境变量名称', false)
// command 注册命令
program
.command('clone <source> [destination]')
.description('clone a repository')
.option('-f, --force', '是否强制拷贝')
.action((source, destination, cmdObj) => {
console.log('do clone', source, destination, cmdObj.force)
})
// addCommand 注册子命令
const service = new commander.Command('service')
service
.command('start [port]')
.description('start service at some port')
.action((prot, cmdObj) => {
console.log('do server start', prot)
})
service
.command('stop')
.description('stop service')
.action(() => {
console.log('do server stop')
})
program.addCommand(service)
// program
// .command('install [name]', 'install package', {
// executableFile: 'weilai-cli',
// // isDefault: true,
// // hidden: true
// })
// .alias('i')
// 高级定制:自定义help信息
// console.log(program.outputHelp())
// console.log(program.helpInformation())
program.helpInformation = () => ''
program.on('--help', () => {
console.log('your help information')
})
program.on('option:debug', () => {
console.log('debug')
})
program.on('command:*', (obj) => {
console.log('未知的命令', obj)
const availableCommands = program.commands.map(cmd => cmd.name())
console.log('可用的命令', availableCommands)
})
// program
// .arguments('<cmd> [options]')
// .description('test command', {
// cmd: 'command to run',
// options: 'options for command'
// })
// .action((cmd, options) => {
// console.log(cmd, 'arguments')
// })
program
.parse(process.argv)
// console.log(program.debug)
// console.log(program.env)
// console.log(program.opts())
'use strict';
const cp = require('child_process')
const path = require('path')
const log = require('@weilai-cli/log')
const Package = require('@weilai-cli/package')
const SETTINGS = {
init: '@weilai-cli/init'
}
const CHCHE_DIR = 'dependencies'
async function exec(...argm) {
let storePath, pkg
let targetPath = process.env.CLI_TARGET_PATH
const homePath = process.env.CLI_HOME_PATH
log.verbose('targetPath', targetPath)
log.verbose('homePath', homePath)
const cmdObj = argm[argm.length - 1]
const cmdName = cmdObj.name()
const packageName = SETTINGS[cmdName]
const packageVersion = 'latest'
if(!targetPath) {
targetPath = path.resolve(homePath, CHCHE_DIR)
storePath = path.resolve(homePath, 'node_modules')
log.verbose('targetPath', targetPath)
log.verbose('storePath', storePath)
pkg = new Package({
targetPath,
storePath,
packageName,
packageVersion
})
if(await pkg.exists()) {
// 更新
log.verbose('package', '更新')
await pkg.update()
} else {
// 安装
log.verbose('package', '安装')
await pkg.install()
}
} else {
pkg = new Package({
targetPath,
packageName,
packageVersion
})
}
const rootFile = pkg.getRootFile()
if(rootFile) {
try {
// 当前进程
// rootFile && require(rootFile)(argm)
// 子进程
const o = Object.create(null)
Object.keys(cmdObj).forEach(key => {
if(
cmdObj.hasOwnProperty(key) &&
!key.startsWith('_') &&
key !== 'parent'
) {
o[key] = cmdObj[key]
}
})
argm[argm.length - 1] = o
const code = `require('${rootFile}')(${JSON.stringify(argm)})`
const child = spawn('node', [ '-e', code ], {
cwd: process.cwd(),
stdio: 'inherit' // 这个属性是把子进程的输出流直接挂载到父进程
})
child.on('error', e => {
log.error(e.message)
process.exit(1)
})
child.on('exit', e => {
log.verbose('命令执行成功:', e)
process.exit(e)
})
} catch(err) {
log.error(err.message)
}
}
}
function spawn(command, args, options) {
const win32 = process.platform === 'win32'
const cmd = win32 ? 'cmd' : command
const cmdArgs = win32 ? ['/c'].concat(command, args) : args
return cp.spawn(cmd, cmdArgs, options || {})
}
module.exports = exec;
// package
'use strict';
const path = require('path')
const pkgDir = require('pkg-dir')
const fsExtra = require('fs-extra')
const npminstall = require('npminstall')
const pathExists = require('path-exists')
const { isObject } = require('@weilai-cli/utils')
const formatPath = require('@weilai-cli/format-path')
const {
getDefaultRegistry,
getNpmLatestVersion
} = require('@weilai-cli/get-npm-info')
class Package {
constructor(options) {
if(!options) throw new Error('Package 类的 options 参数不能为空')
if(!isObject(options)) throw new Error('Package 类的 options 参数必须是对象')
// 路径
this.targetPath = options.targetPath
// 存储路径
this.storePath = options.storePath
// 名称
this.packageName = options.packageName
// 版本号
this.packageVersion = options.packageVersion
// 缓存目录的前缀
this.cacheFilePathPrefix = this.packageName.replace('/', '_')
}
async prepare() {
if(this.storePath && !pathExists.sync(this.storePath)) {
fsExtra.mkdirpSync(this.storePath)
}
if(this.packageVersion === 'latest') {
this.packageVersion = await getNpmLatestVersion(this.packageVersion)
}
}
// 获取缓存文件的路径
get cacheFilePath() {
return path.resolve(
this.storePath,
`_${this.cacheFilePathPrefix}@${this.packageVersion}@${this.packageName}`
)
}
getSpecificCacheFilePath(packageVersion) {
return path.resolve(
this.storePath,
`_${this.cacheFilePathPrefix}@${packageVersion}@${this.packageName}`
)
}
// 判断当前 package 是否存在
async exists() {
if(this.storePath) {
await this.prepare()
console.log('cacheFilePath', this.cacheFilePath)
return pathExists.sync(this.cacheFilePath)
} else {
return pathExists.sync(this.targetPath)
}
}
// 安装 package
install() {
npminstall({
root: this.targetPath,
storeDir: this.storePath,
registry: getDefaultRegistry(),
pkgs: [{
name: this.packageName,
version: this.packageVersion
}]
})
}
// 更新 package
async update() {
await this.prepare()
// 1. 获取最新的 npm 模块的版本号
const latestPackageVersion = await getNpmLatestVersion(this.packageName)
// 2. 查询最新版本号对应的路径是否存在
const latestFilePath = this.getSpecificCacheFilePath(latestPackageVersion)
// 3. 如果不存在,则直接安装最新版本
if(!pathExists.sync(latestFilePath)) {
npminstall({
root: this.targetPath,
storeDir: this.storePath,
registry: getDefaultRegistry(),
pkgs: [{
name: this.packageName,
version: latestPackageVersion
}]
})
this.packageVersion = latestPackageVersion
}
}
// 获取入口文件
getRootFile() {
function _getRootFile(targetPath) {
// 1. 获取 package.json 所在的目录
const dir = pkgDir.sync(targetPath)
if(dir) {
// 2. 读取 package.json
const pkgFile = require(path.resolve(dir, 'package.json'))
// 3. 寻找 main / bin
if(pkgFile && pkgFile.main) {
// 4. 路径的兼容
return formatPath(path.resolve(dir, pkgFile.main))
}
}
return null
}
return this.storePath
? _getRootFile(this.storePath)
: _getRootFile(this.targetPath)
}
}
module.exports = Package;// package
'use strict';
const path = require('path')
const pkgDir = require('pkg-dir')
const fsExtra = require('fs-extra')
const npminstall = require('npminstall')
const pathExists = require('path-exists')
const { isObject } = require('@weilai-cli/utils')
const formatPath = require('@weilai-cli/format-path')
const {
getDefaultRegistry,
getNpmLatestVersion
} = require('@weilai-cli/get-npm-info')
class Package {
constructor(options) {
if(!options) throw new Error('Package 类的 options 参数不能为空')
if(!isObject(options)) throw new Error('Package 类的 options 参数必须是对象')
// 路径
this.targetPath = options.targetPath
// 存储路径
this.storePath = options.storePath
// 名称
this.packageName = options.packageName
// 版本号
this.packageVersion = options.packageVersion
// 缓存目录的前缀
this.cacheFilePathPrefix = this.packageName.replace('/', '_')
}
async prepare() {
if(this.storePath && !pathExists.sync(this.storePath)) {
fsExtra.mkdirpSync(this.storePath)
}
if(this.packageVersion === 'latest') {
this.packageVersion = await getNpmLatestVersion(this.packageVersion)
}
}
// 获取缓存文件的路径
get cacheFilePath() {
return path.resolve(
this.storePath,
`_${this.cacheFilePathPrefix}@${this.packageVersion}@${this.packageName}`
)
}
getSpecificCacheFilePath(packageVersion) {
return path.resolve(
this.storePath,
`_${this.cacheFilePathPrefix}@${packageVersion}@${this.packageName}`
)
}
// 判断当前 package 是否存在
async exists() {
if(this.storePath) {
await this.prepare()
console.log('cacheFilePath', this.cacheFilePath)
return pathExists.sync(this.cacheFilePath)
} else {
return pathExists.sync(this.targetPath)
}
}
// 安装 package
install() {
npminstall({
root: this.targetPath,
storeDir: this.storePath,
registry: getDefaultRegistry(),
pkgs: [{
name: this.packageName,
version: this.packageVersion
}]
})
}
// 更新 package
async update() {
await this.prepare()
// 1. 获取最新的 npm 模块的版本号
const latestPackageVersion = await getNpmLatestVersion(this.packageName)
// 2. 查询最新版本号对应的路径是否存在
const latestFilePath = this.getSpecificCacheFilePath(latestPackageVersion)
// 3. 如果不存在,则直接安装最新版本
if(!pathExists.sync(latestFilePath)) {
npminstall({
root: this.targetPath,
storeDir: this.storePath,
registry: getDefaultRegistry(),
pkgs: [{
name: this.packageName,
version: latestPackageVersion
}]
})
this.packageVersion = latestPackageVersion
}
}
// 获取入口文件
getRootFile() {
function _getRootFile(targetPath) {
// 1. 获取 package.json 所在的目录
const dir = pkgDir.sync(targetPath)
if(dir) {
// 2. 读取 package.json
const pkgFile = require(path.resolve(dir, 'package.json'))
// 3. 寻找 main / bin
if(pkgFile && pkgFile.main) {
// 4. 路径的兼容
return formatPath(path.resolve(dir, pkgFile.main))
}
}
return null
}
return this.storePath
? _getRootFile(this.storePath)
: _getRootFile(this.targetPath)
}
}
module.exports = Package;
// init
'use strict';
const log = require("@weilai-cli/log")
const Command = require('@weilai-cli/command')
class initCommand extends Command {
init() {
this.projectName = this._argv[0] || ''
this.force = !!this._cmd.force
log.verbose('projectName', this.projectName)
log.verbose('force',this.force)
}
exec() {
// init 的业务逻辑
console.log('init 的业务逻辑')
}
}
function init(argv) {
return new initCommand(argv)
}
module.exports = init
module.exports.initCommand = initCommand
判断目标目录是否为空,如果不为空要提示用户是否继续创建,如果继续创建,则要提示用户是否清空目标目录下的所有文件
// 准备阶段
async prepare() {
const localPath = process.cwd()
// 0. 判断项目模板是否存在
const spinner = spinnerStart('正在获取模板信息...')
const template = this.template = await getProjectTemplate()
spinner.stop(true)
log.verbose('template', template)
if(!Array.isArray(template) || template.length === 0) {
throw new Error('项目模板不存在')
}
// 1. 判断当前目录是否为空
if(!this.isCwdEmpty(localPath)) {
// 2. 询问是否启动强制更新
let ifContinue
if(!this.force) {
ifContinue = (await inquirer.prompt({
type: 'confirm',
name: 'ifContinue',
default: false,
message: '当前文件夹不为空,是否继续创建项目?'
})).ifContinue
// 终止流程
if(!ifContinue) return false
}
if( ifContinue || this.force ) {
// 二次确认
const { confirmDelete } = await inquirer.prompt({
type: 'confirm',
name: 'confirmDelete',
default: false,
message: '是否确认清空当前目录下的文件?'
})
if(confirmDelete) {
// 清空当前目录
const spinner = spinnerStart('正在清空当前目录...')
fsExtra.emptyDirSync(localPath)
spinner.stop(true)
}
}
}
// 项目的基本信息
return this.getProjectInfo()
}
// 获取项目信息 async getProjectInfo() { function isValidName(v) { return /^[a-zA-Z]+([-][a-zA-Z][a-zA-Z0-9]|[_][a-zA-Z][a-zA-Z0-9]|[a-zA-Z0-9])*$/.test(v) }
let projectInfo = {}
// 1. 选择创建项目或者组件
const { type } = await inquirer.prompt({
type: 'list',
name: 'type',
message: '请选择初始化类型',
default: TYPE_PROJECT,
choices: [
{ name: '项目', value: TYPE_PROJECT },
{ name: '组件', value: TYPE_COMPONENT}
]
})
log.verbose('project class', type)
const title = type === TYPE_PROJECT ? '项目' : '组件'
this.template = this.template.filter(template => template.tag.includes(type))
const projectPrompt = [
{
type: 'input',
name: 'projectName',
message: `请输入${title}名称`,
default: this.projectName ? this.projectName : '',
validate: function(v) {
const done = this.async()
setTimeout(() => {
// 1. 首字符必须为英文
// 2. 尾字符必须为英文和数字
// 3. 字符仅允许'-_'
if(!isValidName(v)) {
return done(`请输入合法的${title}名称`)
}
done(null, true)
})
},
filter: (v) => {
return v
}
},
{
type: 'input',
name: 'projectVersion',
message: `请输入${title}版本号`,
default: '1.0.0',
validate: function(v) {
const done = this.async()
setTimeout(() => {
if(!sermver.valid(v)) {
return done(`请输入合法的版本号`)
}
done(null, true)
})
},
filter: (v) => {
if(!!sermver.valid(v)) {
return sermver.valid(v)
}
return v
}
}, {
type: 'list',
name: 'projectTemplate',
message: `请选择${title}模板`,
choices: this.createTemplateChoice()
}
]
// 2. 获取项目的基本信息
if(type === TYPE_PROJECT) {
const project = await inquirer.prompt(projectPrompt)
projectInfo = { type, ...projectInfo, ...project }
} else if(type === TYPE_COMPONENT) {
const descriptionPrompt = {
type: 'input',
name: 'componentDescription',
message: '请输入组件描述信息',
default: '',
validate: function(v) {
const done = this.async()
setTimeout(() => {
if(!(v)) {
return done('请输入组件描述信息')
}
done(null, true)
})
}
}
projectPrompt.push(descriptionPrompt)
const project = await inquirer.prompt(projectPrompt)
projectInfo = { type, ...projectInfo, ...project }
}
if(projectInfo.projectName) {
projectInfo.name = require('kebab-case')(projectInfo.projectName).replace(/^-/, '')
}
if(projectInfo.projectVersion) {
projectInfo.version = projectInfo.projectVersion
}
if(projectInfo.componentDescription) {
projectInfo.description = projectInfo.componentDescription
}
return projectInfo
}
// 下载模板
async downloadTemplate() {
const { projectTemplate } = this.projectInfo
this.templateInfo = this.template.find(item => item.npmName === projectTemplate)
const targetPath = path.resolve(userHome, '.weilai-cli', 'template')
const storePath = path.resolve(userHome, '.weilai-cli', 'template', 'node_modules')
const { npmName: packageName, version: packageVersion } = this.templateInfo
const templateNpm = this.templateNpm = new Package({
targetPath,
storePath,
packageName,
packageVersion
})
// 判断 package 是否存在
let flag = await templateNpm.exists()
const spinner = spinnerStart('正在下载模板...')
await sleep()
try {
if(!flag) {
// 不存在 安装
await templateNpm.install()
} else {
// 存在 更新
await templateNpm.update()
}
} catch (e) {
throw e
} finally {
spinner.stop(true)
flag ? log.success('更新模板成功') : log.success('下载模板成功')
}
}
const EventEmitter = require('events')
const readline = require('readline')
const MuteStream = require('mute-stream')
const ansiEscapes = require('ansi-escapes')
const { fromEvent } = require('rxjs')
const option = {
type: 'list',
name: 'name',
message: 'select your name:',
choices: [
{ name: 'sam', value: 'sam' },
{ name: 'shuangyue', value: 'sy' },
{ name: 'zhangxuan', value: 'zx'}
]
}
function Prompt(option) {
return new Promise((resolve, reject) => {
try {
const list = new List(option)
list.render()
list.on('exit', (answers) => {
resolve(answers)
})
} catch (e) {
reject(e)
}
})
}
class List extends EventEmitter {
constructor(option) {
super()
this.name = option.name
this.message = option.message
this.choices = option.choices
this.input = process.stdin
const ms = new MuteStream()
ms.pipe(process.stdout)
this.output = ms
this.rl = readline.createInterface({
input: this.input,
output: this.output
})
this.selected = 0
this.height = 0
this.keypress = fromEvent(this.rl.input, 'keypress')
.forEach(this.onKeypress)
this.haveSelected = false // 是否已经选择完毕
}
onKeypress = (keymap) => {
const key = keymap[1]
if(key.name === 'down') {
this.selected++
if(this.selected >= this.choices.length) {
this.selected = 0
}
this.render()
} else if(key.name === 'up') {
this.selected--
if(this.selected < 0) {
this.selected = this.choices.length - 1
}
this.render()
} else if(key.name === 'return') {
this.haveSelected = true
this.render()
this.close()
this.emit('exit', this.choices[this.selected])
}
}
clean() {
const emptyLines = ansiEscapes.eraseLines(this.height)
this.output.write(emptyLines)
}
close() {
this.output.unmute()
this.rl.output.end()
this.rl.pause()
this.rl.close()
}
getContent = () => {
if (!this.haveSelected) {
let title = '\x1B[32m?\x1B[39m \x1B[1m' + this.message + '\x1B[22m\x1B[0m \x1B[0m\x1B[2m(Use arrow keys)\x1B[22m\n';
this.choices.forEach((choice, index) => {
if (index === this.selected) {
if (index === this.choices.length - 1) {
title += '\x1B[36m❯ ' + choice.name + '\x1B[39m ';
} else {
title += '\x1B[36m❯ ' + choice.name + '\x1B[39m \n';
}
} else {
if (index === this.choices.length - 1) {
title += ` ${choice.name} `;
} else {
title += ` ${choice.name} \n`;
}
}
});
this.height = this.choices.length + 1;
return title;
} else {
const name = this.choices[this.selected].name;
let title = '\x1B[32m?\x1B[39m \x1B[1m' + this.message + '\x1B[22m\x1B[0m \x1B[36m' + name + '\x1B[39m\x1B[0m \n';
return title;
}
};
render() {
this.output.unmute()
this.clean()
this.output.write(this.getContent())
this.output.mute()
}
}
Prompt(option)
.then(answers => {
console.log(answers)
})
async installTemplate() {
if(this.templateInfo) {
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('项目模板信息类型无法识别')
}
} else {
throw new Error('项目模板信息不存在')
}
}
最后会为模板进行依赖安装和执行启动命令。
async installNormalTemplate() {
log.verbose('安装标准模板')
log.verbose('templateNpm', this.templateNpm)
// 拷贝模板代码到当前目录
const spinner = spinnerStart('正在安装模板...')
const templatePath = path.resolve(this.templateNpm.cacheFilePath, 'template')
const targetPath = process.cwd()
await sleep()
try {
fsExtra.ensureDirSync(templatePath) // 确保目录存在
fsExtra.ensureDirSync(targetPath) // 确保目录存在
fsExtra.copySync(templatePath, targetPath) // 拷贝到 targetPath 目录下
} catch (e) {
throw e
} finally {
spinner.stop(true)
log.success('模板安装成功')
}
const templateIgnore = this.templateInfo.ignore || []
const ignore = ['**/node_modules/**', ...templateIgnore]
await this.ejsRender({ignore})
const { installCommand, startCommand } = this.templateInfo
let installCmdRet, startCmdRet
// 依赖安装
await this.execCommand(installCommand, '依赖安装失败')
// 启动命令执行
await this.execCommand(startCommand, '启动命令执行失败')
}
async installCustomTemplate() {
log.verbose('安装自定义模板')
log.verbose('templateNpm', this.templateNpm)
if(await this.templateNpm.exists()) {
const rootFile = this.templateNpm.getRootFile()
log.verbose('rootFile', rootFile)
if(fs.existsSync(rootFile)) {
log.notice('开始执行自定义模板安装')
const templatePath = path.resolve(this.templateNpm.cacheFilePath, 'template')
const options = {
targetPath: process.cwd(),
sourcePath: templatePath,
templateInfo: this.templateInfo,
projectInfo: this.projectInfo
}
const code = `require('${rootFile}')(${JSON.stringify(options)})`
await spawnAsync('node', ['-e', code], { stdio: 'inherit', cwd: process.cwd()})
log.success('自定义模板安装成功')
} else {
throw new Error('自定义模板入口文件不存在!')
}
}
}
ejs 标签如下所示:
<% '脚本' 标签,用于流程控制,无输出。
<%_ 删除其前面的空格符
<%= 输出数据到模板(输出是转义 HTML 标签)
<%- 输出非转义的数据到模板
<%# 注释标签,不执行、不输出内容
<%% 输出字符串 '<%'
%> 一般结束标签
-%> 删除紧随其后的换行符
_%> 将结束标签后面的空格符删除
脚手架模板渲染,首先通过 glob 获取目标目录下的所有文件,并且过滤掉一些不需要渲染的文件或文件夹,然后通过循环逐一对每个文件进行渲染,渲染后复写该文件。
async ejsRender(options) {
const cwd = process.cwd()
return new Promise((resolve1, reject1) => {
glob('**', {
cwd: cwd,
ignore: options.ignore || '',
nodir: true
}, (err, files) => {
if(err) {
reject1(err)
}
Promise.all(files.map(file => {
const filePath = path.join(cwd, file)
return new Promise((resolve2, reject2) => {
ejs.renderFile(filePath, this.projectInfo, {}, (err, result) => {
if(err) {
reject2(err)
}
fsExtra.writeFileSync(filePath, result)
resolve2(result)
})
})
})).then(() => {
resolve1()
}).catch(err => {
reject1(err)
})
})
})
}
命令执行的原理依旧是使用 spawn 通过对命令字符串的拆分并且去验证它是否是合法的命令。
async execCommand(command, errMsg) {
if(command) {
const cmdOptions = command.split(' ')
const cmd = this.checkCommand(cmdOptions[0])
const args = cmdOptions.splice(1)
const ret = await spawnAsync(cmd, args, {
stdio: 'inherit',
cwd: process.cwd()
})
if(ret !== 0) {
throw new Error(errMsg)
}
return ret
}
throw new Error(`命令不存在`)
}
const cli = yargs(); // 创建一个基础脚手架
cli
// 配置 使用提示
.usage('Usage: weilai-test-cli [command] <options>')
// 配置 脚手架最少要接收一个命令
.demandCommand(1, "最少需要输入一个命令。 通过 --help 查看所有可用的命令和选项。")
// 配置 命令输入错误的时候可以根据输入 推荐合适的命令
.recommendCommands()
// 配置 严格的无法识别的命令也将报告为错误
.strict()
// 配置 发生故障时执行的方法
.fail((err, msg) => {
console.log(err)
})
// 配置 help 和 version 的别名
.alias('h', 'help')
.alias('v', 'version')
// 配置 容器宽度
.wrap(cli.terminalWidth())
// 配置 收尾的文字
.epilogue(
dedent`
哎哟,不错哟!
小伙汁
`
)
// 配置 gameStart 配置 类型为布尔值,描述是五黑走起,别名是 g
.options({
gameStart: {
type: 'boolean',
describe: '五黑走起',
alias: 'g'
}
})
.option('gameEnd', {
type: 'string',
// hidden: true,
describe: '冲冲冲',
alias: 'r'
})
// 配置 分组 把 gameStart 分配到 召唤师峡谷 这个组里面
.group(['gameStart'], '召唤师峡谷:')
.group(['gameEnd'], '慕课网:')
// 配置 命令 当执行 init [name] 命令的时候一系列的行为
.command(
'init [name]', 'Do init a project',
(yargs) => {
// 子命令
yargs
.option('name', {
type: 'string',
describe: 'Name of aproject',
alias: 'n'
})
},
(argv) => {
// 行为
console.log(argv)
}
)
// 配置 命令的第二种方法
.command({
command: 'list',
aliases: ['ls', 'la', 'll'],
describe: 'List local packages',
builder: (yargs) => {
},
handler: (argv) => {
console.log(argv)
}
})
// 解析参数
.parse(argv, context)
npm i -D babel-loader @babel/core @babel/preset-env
npm i -D @babel/plugin-transform-runtime
npm i -D @babel/runtime-corejs3
创建webpack.config.js
const path = require('path');
module.exports = { entry: './bin/core.js', output: { path: path.join(__dirname, '/dist'), filename: 'core.js' }, mode: 'development', target: 'node', // 默认是 web 环境 // 以上四步完成后就可以支持es module // 还想要支持低版本的node, 就需要配置babel-loader转义 module: { rules: [ { test: /.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], plugins: [ [ '@babel/plugin-transform-runtime', { corejs: 3, regenerator: true, useESModules: true, helpers: true } ] ] } } } ] } }
##### 方案二: Node原生支持ES Module
这种方案的实现,所有文件必须以.mjs结尾。文件内必须以ES Module的方式导出或引用
- 创建index.mjs文件
- node版本小于14 node --experimental-modules index.mjs
- node版本大于14 node index.mjs
isDirEmpty(localPath) {
let fileList = fs.readdirSync(localPath)
// 文件过滤的逻辑
fileList = fileList.filter(file => (!file.startsWith('.') && ['node_modules'].indexOf(file) < 0))
return !fileList || fileList.length <= 0
}
const localPath = process.cwd()
if (!this.isDirEmpty(localPath)) {
// 询问是否继续创建 使用到inquirer这个库
// 如果 用户不是强制更新,那么就要询问用户是否继续创建
let ifContinue = false
if (!this.force) {
ifContinue = (
await inquirer.prompt({
type: 'confirm',
name: 'ifContinue',
message: '当前目录不为空,是否继续创建?',
default: false
})
).ifContinue
if (!ifContinue) {
return
}
}
// 不管用户是否是强制更新,最后都会展示这次询问,因为清空当前目录文件是一个非常严谨的操作
if (ifContinue || this.force) {
// 做二次确认
const { confirmDelete } = await inquirer.prompt({
type: 'confirm',
name: 'confirmDelete',
message: '是否确认清空当前目录下的文件?',
default: false
})
if (confirmDelete) {
// 清空当前目录 使用 fse-extra
fse.emptyDirSync(localPath)
}
}
}
// 声明一个对象用来接收项目信息 最后返回的也是这个对象
let projectInfo = {}
// 校验项目名称的正则,封装在一个函数内
function isValidName(v) {
return /^[a-zA-Z]+([-][a-zA-Z][a-zA-Z0-9]*|[_][a-zA-Z][a-zA-Z0-9]*|[a-zA-Z0-9])*$/.test(v)
}
// 默认项目名称是不通过的
let isProjectNameValid = false
// 如果用户在输入命令时的名称符合规则 就直接用这个
if (isValidName(this.projectName)) {
isProjectNameValid = true
projectInfo.projectName = this.projectName
}
// inquirer获取用户想要下载的是组件模板还是项目模板
const { type } = await inquirer.prompt({
type: 'list',
name: 'type',
message: '请选择初始化项目类型?',
default: TYPE_PROJECT,
choices: [
{
name: '项目',
value: TYPE_PROJECT
},
{
name: '组件',
value: TYPE_COMPONENT
}
]
})
// 通过条件过滤对应的模板
this.template = this.template.filter((template) => {
return template.tag.includes(type)
})
const title = type === TYPE_PROJECT ? '项目' : '组件'
// 兼容项目和模板两种情况的交互询问
const projectNamePrompt = {
type: 'input',
name: 'projectName',
message: `请输入${title}名称`,
default: '',
validate: function (v) {
const done = this.async()
// 1.首字符必须为英文字符
// 2.尾字符必须为英文字符或数字,不能为字符
// 3.字符仅允许“-_”
// 4.兼容只有一个字母的情况
setTimeout(function () {
if (!isValidName(v)) {
done(`请输入合法的${title}名称,例:a1 | a_b_c | a1_b1_c1`)
return
}
// Pass the return value in the done callback
done(null, true)
}, 0)
},
filter: function (v) {
return v
}
}
// 这个数组是最后要传给inquirer的参数
const projectPrompt = []
// 如果用户在命令行输入的名称不符合要求,将后来用户输入的名称添加到数组中
if (!isProjectNameValid) {
projectPrompt.push(projectNamePrompt)
}
// 除了项目名称以外 还要知道用户输入的版本号、选择的模板
projectPrompt.push(
{
type: 'input',
name: 'projectVersion',
message: `请输入${title}版本号`,
default: '1.0.0',
validate: function (v) {
const done = this.async()
setTimeout(function () {
if (!!!semver.valid(v)) {
done('请输入合法的项目版本号,例:1.0.0')
return
}
// Pass the return value in the done callback
done(null, true)
}, 0)
return
},
filter: function (v) {
if (!!semver.valid(v)) {
return semver.valid(v)
}
return v
}
},
{
type: 'list',
name: 'projectTemplate',
message: `请选择${title}模板`,
choices: this.createProjectTemplate()
}
)
// 如果用户选择的是项目模板 直接将上面的 projectPrompt 传递给 inquirer 即可 然后用将所有要用到的信息进行拼装,就是 projectInfo
if (type === TYPE_PROJECT) {
const project = await inquirer.prompt(projectPrompt)
projectInfo = {
...projectInfo,
type,
...project
}
} else if (type === TYPE_COMPONENT) {
// 如果用户选择的是组件模板,那么在前面的基础上追问一条描述信息
const descriptionPrompt = {
type: 'input',
name: 'componentDescription',
message: '请输入组件描述信息',
default: '',
validate: function (v) {
const done = this.async()
setTimeout(() => {
if (!v) {
done('请输入组件描述信息')
return
}
done(null, true)
}, 0)
}
}
projectPrompt.push(descriptionPrompt)
const component = await inquirer.prompt(projectPrompt)
projectInfo = {
...projectInfo,
type,
...component
}
}
// 最后对拿到的项目信息进行一些转换 这里在转换项目名称的时候用到了kebab-case这个库,可以将驼峰格式的名称转为连字符格式的
// 生成classname
if (projectInfo.projectName) {
projectInfo.name = projectInfo.projectName
projectInfo.className = require('kebab-case')(projectInfo.projectName)
}
// 生成version
if (projectInfo.projectVersion) {
projectInfo.version = projectInfo.projectVersion
}
// 生成description
if (projectInfo.componentDescription) {
projectInfo.description = projectInfo.componentDescription
}
// 至此,项目基本信息就获取完成了
return projectInfo
在执行一个node代码的时候,默认会向node代码当中注入一些变量:filename 、 dirname 、 require 、 module、exports.
首先,执行lerna命令的时候,会执行node全局下的lerna,即which lerna 指向的: 软连接:/Users/liumingzhou/.nvm/versions/node/v12.16.1, 实际指向:/Users/liumingzhou/.nvm/versions/node/v12.16.1/lib/node_modules/lerna/cli.js[PRATIC]
然后,在webstorm的debug调试中,Node parameters修改为[PRATIC] 地址。 接着,点击调试按钮,我们知道,程序首先进入的文件是[PRATIC]
#!/usr/bin/env node
"use strict";
/* eslint-disable import/no-dynamic-require, global-require */
const importLocal = require("import-local");
if (importLocal(__filename)) {
require("npmlog").info("cli", "using local version of lerna");
} else {
require(".")(process.argv.slice(2));
}
import-local源码:
'use strict';
const path = require('path');
const resolveCwd = require('resolve-cwd');
const pkgDir = require('pkg-dir');
module.exports = filename => {
const globalDir = pkgDir.sync(path.dirname(filename));
const relativePath = path.relative(globalDir, filename);
const pkg = require(path.join(globalDir, 'package.json'));
const localFile = resolveCwd.silent(path.join(pkg.name, relativePath));
return localFile && path.relative(localFile, filename) !== '' ? require(localFile) : null;
};
path.dirname(filename):意思是获取到文件filename的上级目录。
checkInputArgs 检查命令行参数
checkEnv 检查环境变量
checkGlobalUpdate 检查是否需要全局更新