AurorePaladin / AurorePaladin.github.io

个人主页。同时也通过 Issues 记录学习笔记
5 stars 1 forks source link

core模块技术方案 #75

Open AurorePaladin opened 2 years ago

AurorePaladin commented 2 years ago

core模块prepare

AurorePaladin commented 2 years ago

核心库

AurorePaladin commented 2 years ago

工具库

AurorePaladin commented 2 years ago

core主流程

在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是持续编译终端要被占用

AurorePaladin commented 2 years ago

实现脚手架准备过程

  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)
}
AurorePaladin commented 2 years ago

通过 commander 框架实现一个脚手架,包含自定义 option 和 command 功能

#! /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())

可通过 webpack 和 原生两种方式实现 node 对 ES Module 的支持

AurorePaladin commented 2 years ago

脚手架动态命令执行代码

'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
AurorePaladin commented 2 years ago

prepare/准备

// 获取项目信息 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

}

AurorePaladin commented 2 years ago

downloadTemplate/下载模板

首先需要获取到之前收集的各种信息数据,例如:名称、版本、类型、模板;
然后在通过这些数据筛选出 模板的信息 ,并生成 目标、缓存 目录,以及其他配置项信息;
其次创建一个 Package 对象,并且判断 package 是否存在(就是在缓存中是否已经下载了该模板),如果存在则对 ###### Package 对象进行更新模板的操作,如果不存在则对 Package 对象进行下载操作。
// 下载模板
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('下载模板成功')
    }
}
AurorePaladin commented 2 years ago

readline 核心元源码分析

命令行可交互列表组件的核心原理是依赖 readline 去开发的;
通过使用 rxjs 监听 keypress 来对命令行中显示的内容进行重绘;
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)
    })
AurorePaladin commented 2 years ago

项目和组件初始化

AurorePaladin commented 2 years ago

标准安装

AurorePaladin commented 2 years ago

自定义安装

AurorePaladin commented 2 years ago

ejs 渲染模板

ejs 标签如下所示:

<% '脚本' 标签,用于流程控制,无输出。
<%_ 删除其前面的空格符
<%= 输出数据到模板(输出是转义 HTML 标签)
<%- 输出非转义的数据到模板
<%# 注释标签,不执行、不输出内容
<%% 输出字符串 '<%'
%> 一般结束标签
-%> 删除紧随其后的换行符
_%> 将结束标签后面的空格符删除
AurorePaladin commented 2 years ago
命令执行
AurorePaladin commented 2 years ago

理解 yargs 常用 API 和 开发流程

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)
AurorePaladin commented 2 years ago

Node项目如何支持ES Module

方案一: webpack + bable-loader

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
AurorePaladin commented 2 years ago

require的使用场景

require的最终执行结果是在例如.js文件源码外层套一个自执行函数,这个函数的入参就有module, exports, dirname, filename, require等。自执行函数的返回结果为exports或modules.exports的值。
加载模块类型:

Module对象

AurorePaladin commented 2 years ago

脚手架项目创建

判断当前目录(要运行脚手架命令进行模板安装的那个目录)是否为空

可以使用 Node.js 提供的文件系统操作模块(fs)。
isDirEmpty(localPath) {
        let fileList = fs.readdirSync(localPath)
            // 文件过滤的逻辑
        fileList = fileList.filter(file => (!file.startsWith('.') && ['node_modules'].indexOf(file) < 0))
        return !fileList || fileList.length <= 0
    }

强制清空当前目录功能开发

先判断当前目录是否为空,在不为空的情况下使用 inquirer 来询问用户是否要继续创建项目,这里要注意的是 force 参数的获取;但是因为清空文件夹的操作是一个不可逆的操作,要至少询问一次用户是否确认清空。如果用户确认清空,就清空当前目录,用到的库是 fs-extra。
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)
        }
    }
}

获取项目基本信息功能的开发

在清空了文件夹以后,我们要询问用户一些基本信息,比如这个项目的名字、版本号以及可能存在的描述信息(组件模板的情况下),将来这些信息都会通过 ejs 模板引擎渲染到 package.json 文件中。
// 声明一个对象用来接收项目信息 最后返回的也是这个对象
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
AurorePaladin commented 2 years ago

关于 import-local执行流程深度分析

import-local的作用是:当我们的项目当中本地存在一个脚手架命令,同时全局在node当中也存在一个脚手架命令的时候,优先选用本地的node_modules中的版本。

AurorePaladin commented 2 years ago

import-local 源码分析

1