写个 cli 快速生成项目模板

2,322 阅读4分钟

当我们想要开始一个新项目的时候,首先要搭建一个脚手架,再添加一些基础功能,最后在试着运行或构建项目。由于前端工程化日益完善,所以搭建新项目的一系列操作往往需要不少时间和精力。幸好社区中有不少优秀的 cli 工具和模板工程,比如 create-react-appcreate-viteAwesome Vite,我们可以使用它们来快速搭建新项目。

平时前端开发中,常见的项目类型有 web 站点、SDK、组件库、h5 项目等等。各个类型的项目模板肯定也是有所差异的,为了方便,我们可以写个 cli 工具来利用社区模板或 cli 来快速创建项目模板。

CLI 工具 npm 包简介

在编写 CLI 工具前,我们先介绍 CLI 工具中用到的工具库。

  • commander: 定义命令行指令,在输入自定义的命令行的时候,会去执行相应的操作
  • inquirer: 可以在命令行询问用户问题,并且可以记录用户回答选择的结果
  • fs-extra: 是 fs 的一个扩展,提供了非常多的便利 API,同时添加了 promise 的支持
  • chalk: 可以美化控制台的输出
  • ora: 控制台的 loading 动画
  • degit: 下载远程模板
  • cross-spawn: 跨平台的命令调用模块

实现 CLI

注册和运行 CLI

首先创建文件夹和初始化项目

$ mkdir quick-template-cli
$ cd quick-template-cli 
$ npm init -y

接下来创建个 bin 目录,在 bin 目录下创建 index.js

#! /usr/bin/env node
console.log('quick-template-cli')

#! /usr/bin/env node 代表这个文件用 node 执行。

然后打开 package.json,模块化方案使用 ESModule,加上配置 "type": "module",在增加 bin 字段注册命令。

// package.json
{
    ...,
    "type": "module",
    "bin": {
        "quick-template": "./bin/index.js",
        "qt": "./bin/index.js"
    },
    ...
}

可以看到我们注册了两个命令,一个全称 quick-template,一个缩写 qt

最后使用 npm link 把包链接到全局进行调试,在项目根目录下执行 npm link,执行完成后命令行输入 qt 即可看到控制台输出 quick-template-cli

基础命令

一般 CLI 工具都会有帮助和查看版本的命令,我们使用 commander 来添加,同时使用 chalk 美化输出。先安装依赖 npm i commander chalk --save

接着我们引入 commander,实例化一个对象,调用对象的相关方法初始化 CLI 常见命令

import { Command } from 'commander'
import chalk from 'chalk'

const program = new Command()

program
  .name('quick-template')
  .description('Use community cli to create')
  .version('0.0.1')

program.parse()

完成名称、描述和版本定义后,我们可以在命令行输入 qt --helpqt --version ,控制台会输出相应信息。

cli1.png

我们可以在使用 help 命令的时候补充一些信息,使用 chalk 美化关键输出。

program
  .on('--help', () => {
    console.log(`\r\nRun ${chalk.cyan(`qt <command> --help`)} for detailed usage of given command\r\n`)
  })

cli2.png

create 命令

现在我们开始创建 create 命令,先简单定义命令,输出命令带的参数。

program.command('create <name>')
  .description('Create a new project')
  .action(async (name) => {
    console.log(name)
  })

此时可以执行 qt create demo-project,控制台会输出 demo-project。

简单完成 create 命令定义后,我们梳理下 create 命令的逻辑:

  1. 询问用户要创建什么类型项目
  2. 根据用户选择做不同处理 2.1. 如果是普通网站项目,我们直接使用 create-vite 命令 2.2. 如果是 SDK,我们去下载 rollup 官方提供的一个模板工程 2.3. 如果是组件库,我们使用 dumi 来初始化项目

首先处理普通网站模板的创建,安装对应包 npm i inquirer cross-spawn --save

...
import inquirer from 'inquirer'
import spawn from 'cross-spawn'
...

program.command('create <name>')
  .description('Create a new project')
  .action(async (name) => {
    const { action } = await inquirer.prompt([
      {
        name: 'action',
        type: 'list',
        message: 'What type of project it is',
        choices: [
          { name: 'Project', value: 'Project' },
          { name: 'SDK', value: 'SDK' },
          { name: 'Components', value: 'Components' }
        ]
      }
    ])
    if (action === 'Project') {
      spawn('npm', ['create', 'vite@latest', name], { stdio: 'inherit' })
      return
    }
  })

代码逻辑比较简单,使用 inquirer.prompt 方法询问用户项目类型,如果是 Project,通过 spawn 调用 npm create vite@latest project-name 命令,后续流程就是 create-vite 的流程。

cli3.png

接着是 SDK 模板的下载。在创建网站项目模板的时候, create-vite 内部会处理文件夹已存在问题,所以无需我们处理。不过下载 SDK 模板或者组件库模板初始化前,我们就需要自己判断当前文件夹是否存在。

处理逻辑前先引入对应包 npm i degit fs-extra ora --save

...
import path from 'path'
import degit from 'degit'
import fs from 'fs-extra'
import ora from 'ora'
...

program.command('create <name>')
  .description('Create a new project')
  .action(async (name) => {
    ...
    const targetDir = path.join(process.cwd(), name)
    if (fs.existsSync(targetDir)) {
      // 询问用户是否确定要覆盖
      let { action } = await inquirer.prompt([
        {
          name: 'action',
          type: 'list',
          message: 'Target directory already exists Pick an action:',
          choices: [
            { name: 'Overwrite', value: 'overwrite' },
            { name: 'Cancel', value: false }
          ]
        }
      ])

      if (!action) {
        return
      } else if (action === 'overwrite') {
        await fs.remove(targetDir)
      }
    }
  })

通过 process.cwd()name 拼装出目标文件夹,如果文件夹已存在,询问用户是否要覆盖文件夹,确定要覆盖的话就清空文件夹,否则直接退出结束。

判断完文件夹是否存在后,我们使用 degit 下载项目

program.command('create <name>')
  .description('Create a new project')
  .action(async (name) => {
    ...
    if (action === 'SDK') {
      const spinner = ora('waiting download template')
      spinner.start()
      const emitter = degit('github:rollup/rollup-starter-lib')
      await emitter.clone(targetDir)
        .then(() => {
          spinner.succeed('download template succeed.')
        })
        .catch(() => {
          spinner.fail('Request failed...')
        })
      return
    }
  })

使用 ora 初始化一个 loading 对象,展示 loading ,然后指定下载的模板工程并发起下载到指定目录。下载成功或者下载失败都给与用户提示。

cli4.gif

最后处理组件库的初始化,这边使用了 dumi 来初始化组件库。dumi 官方的初始化步骤:

# 先找个地方建个空目录。
$ mkdir myapp && cd myapp

# 通过官方工具创建项目,选择你需要的模板
$ npx create-dumi

我们需要把上面步骤在 CLI 中实现

program.command('create <name>')
  .description('Create a new project')
  .action(async (name) => {
    ...
    if (action === 'Components') {
      spawn.sync('mkdir', [targetDir], { stdio: 'inherit' })
      spawn('npx', ['create-dumi'], { cwd: targetDir, stdio: 'inherit' })
      return
    }
  })

来看下代码实现,首先使用 spawn 同步创建文件夹,然后执行 npx create-dumi,注意需要在指定 targetDir 中执行。

cli5.png

至此我们就实现了所有功能。

总结

通过上述的案例,我们可以认识到 CLI 工具中使用到的一些常见的库,也可以看到一些优秀的社区 CLI 工具或模板工程。像 Awesome Vite 中各个模板,你可以使用 degit 直接下载,当然也可以像上述案例中的 CLI 工具一样,自己做个集成。

最后如果你有一些不错的模板合集,欢迎在评论区分享。

本文正在参加「金石计划」