1、脚手架命令注册
//注册命令function registerCommander() {program.name(Object.keys(pkg.bin)[0]).usage('<command> [options]').version(pkg.version).option('-d,--debug', 'Is debugging mode turned on?', false)//初始化模式program.command('init [name]').alias('i') //添加别名.option('-f,--force', '是否强制初始化').action(init)//注册debug模式program //记得加上program 否则init 命令的name参数打印不对.on('option:debug', () => {if (program.opts()?.debug) {process.env.LOG_LEVEL = 'verbose'} else {process.env.LOG_LEVEL = 'info'}log.level = process.env.LOG_LEVELlog.verbose('test debug')})// 监听未注册的所有命令program.on('command:*', obj => {const availableCommand = program.commands.map(command => command.name)log.info(colors.red('未知的命令 ' + obj[0]))if (availableCommand.length) {log.info(colors.blue('支持的命令 ' + availableCommand.join(',')))}}).parse(process.argv)//判断是否输入命令, 没输入则显示帮助文档if (process.args && program.args.length < 1) {program.outputHelp()}}
lerna create @haha-cli-dev/init
'use strict'module.exports = initfunction init(projectName, cmdObj) {console.log('init', projectName, cmdObj)}
2、当前脚手架架构痛点

问题:
1、脚手架安装速度慢:所有的package都集成在cli里,因此当命令较多,会减慢脚手架的安装速度。
2、灵活性差:init命令只能使用@haha-cli-dev/init包,对于大公司而言,每个团队的init可能各不相同,需要init命令动态化。虽然增加了脚手架的复杂度,但是大大提升了脚手架的可扩展性,将脚手架框架和业务逻辑解耦。
3、脚手架架构优化
4、是否执行本地代码
program.option('-tp,--targetPath <targetPath>', '是否指定本地调试文件路径?').on('option:targetPath', () => {//本地调试代码地址process.env.CLI_TARGET_PATH = program._optionValues.targetPath})
lerna create exec
将初始化时改成exec
//初始化模式program.command('init [name]').description('初始化项目').alias('i') //添加别名.option('-f,--force', '是否强制初始化').action(exec)
lerna create packages 移到models中
'use strict'class Packages {constructor() {console.log('Packages')}}module.exports = Packages
exec中引用packages的内容
'use strict'const pkg = require('@haha-cli-dev/packages')function exec() {const _pkg = new pkg()console.log(_pkg)console.log(process.env.CLI_TARGET_PATH)console.log(process.env.CLI_HOME)//1、targetPath ->modulePath(通过targetPath 拿到实际的modulePath)//2、modulePath ->Package(npm模块) 将modulePath生成一个通用的package//3、Package.getRootFIle(获取入口文件) 这样以后扩展直接处理package的逻辑就可以,而不需要将getRootFIle暴露在外面//封装--->复用}module.exports = exec
5、通用类packages
'use strict';const { isObject } = require('@hzw-cli-dev/utils');const { getRegister, getLatestVersion } = require('@hzw-cli-dev/get-npm-info');const formatPath = require('@hzw-cli-dev/format-path');const npmInstall = require('npminstall');const fse = require('fs-extra');const pathExists = require('path-exists').sync;const pkgDir = require('pkg-dir').sync;const path = require('path');// Package 类 管理模块class Package {/*** @description: 构造函数* @param {*} options 用户传入的配置信息* @return {*}*/constructor(options) {if (!options) {throw new Error('Package 类的参数不能为空!');}if (!isObject(options)) {throw new Error('Package 类的参数必须是对象类型!');}// 获取 targetPath ,如果没有 则说明不是一个本地的packagethis.targetPath = options.targetPath;// 模块安装位置 缓存路径this.storeDir = options.storeDir;// package 的 namethis.packageName = options.packageName;// package 的 Versionthis.packageVersion = options.packageVersion;// 缓存路径的前缀this.cacheFilePathPrefix = this.packageName.replace('/', '_');}/*** @description: 准备工作* @param {*}* @return {*}*/async prepare() {}/*** @description: 获取当前模块缓存路径* @param {*}* @return {*}*/get cacheFilePath() {}/*** @description: 获取最新版本模块缓存路径* @param {*}* @return {*}*/getSpecificFilePath(packageVersion) {}/*** @description: 判断当前 package 是否存在* @param {*}* @return {*}*/async exists() {}/*** @description: 安装 package* @param {*}* @return {*}*/async install() {}/*** @description: 更新 package* @param {*}* @return {*}*/async update() {}/*** @description:获取入口文件的路径* 1.获取package.json所在的目录 pkg-dir* 2.读取package.json* 3.找到main或者lib属性 形成路径* 4.路径的兼容(macOs/windows)* @param {*}* @return {*}*/getRootFilePath() {}module.exports = Package;
6、npmInstall用法
const npminstall = require('npminstall');(async () => {await npminstall({//安装根目录root: process.cwd(),pkgs: [{ name: 'foo', version: '~1.0.0' },],registry: 'https://registry.npmjs.org',//安装缓存目录storeDir: root + 'node_modules'});})().catch(err => {console.error(err);});
7、Command
'use strict'const semver = require('semver')const colors = require('colors')const LOWEST_NODE_VERSION = '12.0.0'class Command {constructor(argv) {if (!argv || argv.length === 0 || !argv[0]) {throw new Error('参数不能为空')}if (!Array.isArray(argv)) {throw new Error('参数必须是数组')}let runner = new Promise((resolve, reject) => {let chain = Promise.resolve()chain = chain.then(() => this.checkNodeVersion()).then(() => this.checkArgs(argv)).then(() => this.init()).then(() => this.exec()).catch(error => console.log(error.message))})}//检查node版本checkNodeVersion() {const currentVersion = process.versionif (!semver.gte(currentVersion, LOWEST_NODE_VERSION)) {throw new Error(colors.red('错误:node版本过低'))}}//初始化参数checkArgs(argv) {this._cmd = argv[arguments.length - 1]this._argv = argv.slice(0, argv.length - 1)}init() {throw new Error('command 必须拥有一个 init 方法')}exec() {throw new Error('command 必须拥有一个 exec 方法')}}module.exports = Command
8、initCommand
'use strict'const Command = require('@haha-cli-dev/command')class initCommand extends Command {init() {this.projectName = this._argv[0]console.log('projectName', this.projectName)}}function init(argv) {return new initCommand(argv)}function exec() {}module.exports = initmodule.exports.initCommand = initCommand
9、exec
'use strict'const path = require('path')const childProcess = require('child_process')const packages = require('@haha-cli-dev/packages')const log = require('@haha-cli-dev/log')const SETTINGS = {init: '@haha-cli-dev/init'}const CACHE_DIR = 'dependencies'async function exec(...argv) {log.level = process.env.LOG_LEVELlet targetPath = process.env.CLI_TARGET_PATHconst homePath = process.env.CLI_HOME_PATHlet storeDir = ''const cmdObj = arguments[arguments.length - 1]const packageName = SETTINGS[cmdObj._name]let pkg//是否执行本地代码if (!targetPath) {targetPath = path.resolve(homePath, CACHE_DIR)storeDir = path.resolve(targetPath, 'node_modules')log.verbose('targetPath', targetPath)log.verbose('storeDir', storeDir)pkg = new packages({targetPath,storeDir,packageName,packageVersion: 'latest'})if (await pkg.exists()) {//更新pkg.update()} else {//初始化pkg.install()}} else {pkg = new packages({targetPath,packageName,packageVersion: 'latest'})}//获取本地代码入口文件const rootFile = pkg.getRootFilePath()log.verbose('rootFile', rootFile)if (rootFile) {//cli已经对全局的异常进行了捕获,为什么在这里仍需要使用try,catch捕获command的异常,因为在使用了program.action异步处理了。异常处理的都需要重新捕获异常try {const cmd = argv[argv.length - 1]const newCmd = Object.create(null)Object.keys(cmd).forEach(key => {if (cmd.hasOwnProperty(key) && !key.startsWith('_') && key !== 'parent') {newCmd[key] = cmd[key]}})argv[argv.length - 1] = newCmdtry {//code是字符串 !!! require(路径(字符串))(参数(字符串))const code = `require('${rootFile}')(${JSON.stringify(argv)})`//在 node 子进程中调用 提高速度const cp = spawn('node', ['-e', code], {cwd: process.cwd(), //cwd 子进程的当前工作目录stdio: 'inherit' //inherit 将相应的stdio传给父进程或者从父进程传入,相当于process.stdin,process.stout和process.stderr})cp.on('error', function (error) {console.log(error.message)process.exit(1)})cp.on('exit', e => {log.verbose('命令执行成功', e)process.exit(e)})} catch (error) {console.log(error.message)}} catch (error) {console.log(error.message)}}function spawn(command, args, options = {}) {const win32 = process.platform === 'win32'const cmd = win32 ? 'cmd' : commandconst cmdArgs = win32 ? ['/c'].concat(command, args) : argsreturn childProcess.spawn(cmd, cmdArgs, options)}}module.exports = exec



