Bootstrap

从0-1逐步搭建一个前端脚手架工具并发布到npm

前言

vue-cli 和 create-react-app 等 cli 脚手架工具用于快速搭建应用,无需手动配置复杂的构建环境。本文介绍如何使用 rollup 搭建一个脚手架工具。

脚手架工具的工作流程简言为:提供远端仓库各种模版 => 用户通过命令选择模版 => 拉取仓库代码

分别对应如下几个重要模块:

  • 配置 打包命令
  • 配置 命令行交互,如 create、-v 等,其中 create 是核心逻辑,用于根据用户的选择拉取远程仓库中相应的初始代码。
  • 发布至 npm

1. 初始化项目

初始化项目

npm init -y

生成:typescript 配置文件 tsconfig.json,在此之前需要确保全局安装了 TypeScript

npm install -g typescript // 如果已经安装,无需理会
npx tsc --init

package.json 中添加依赖

"devDependencies": {
  "@inquirer/prompts": "^3.2.0",
  "@rollup/plugin-commonjs": "^25.0.3", 
  "@rollup/plugin-json": "^6.0.1", 
  "@rollup/plugin-node-resolve": "^15.1.0", 
  "@rollup/plugin-terser": "^0.4.3", 
  "@types/fs-extra": "^11.0.2",
  "@types/lodash": "^4.14.199",
  "@types/node": "^16.18.40",
  "axios": "^1.5.0",
  "chalk": "^4.1.2",
  "commander": "^11.0.0",
  "figlet": "^1.8.0",
  "fs-extra": "^11.1.1",
  "lodash": "^4.17.21",
  "log-symbols": "^4.1.0",
  "ora": "5",
  "progress-estimator": "^0.3.1",
  "pure-thin-cli": "^0.1.8",
  "rollup": "^4.6.1",
  "rollup-plugin-dts": "^5.3.0", 
  "rollup-plugin-esbuild": "^5.0.0",
  "rollup-plugin-node-externals": "^5.1.2", 
  "rollup-plugin-typescript2": "^0.36.0", 
  "simple-git": "^3.19.1",
  "tslib": "^2.6.1",
  "typescript": "^5.2.2"
}

本文用到的所有依赖说明:下文中也会一一介绍依赖的用处与用法

"devDependencies": {
  // 用于命令行交互。
  "@inquirer/prompts": "^3.2.0",
  // Rollup 相关的插件,用于模块打包
  "@rollup/plugin-commonjs": "^25.0.3", // 支持rollup打包commonjs模块
  "@rollup/plugin-json": "^6.0.1", // 支持rollup打包json文件
  "@rollup/plugin-node-resolve": "^15.1.0", // 用于帮助 Rollup 解析和处理 Node.js 模块(Node.js 的 CommonJS 模块规范)
  "@rollup/plugin-terser": "^0.4.3", // Rollup 构建过程中对生成的 JavaScript 代码进行压缩和混淆,以减小最终输出文件的体积。
  // TypeScript 的类型定义文件
  "@types/fs-extra": "^11.0.2",
  "@types/lodash": "^4.14.199",
  "@types/node": "^16.18.40",
   // 用于发起 HTTP 请求。 
  "axios": "^1.5.0",
  // 在命令行中输出彩色文本。
  "chalk": "^4.1.2",
  // 命令行界面的解决方案  
  "commander": "^11.0.0",
  // 优化打印效果
  "figlet": "^1.8.0",
  // 扩展了标准 fs 模块的文件系统操作
  "fs-extra": "^11.1.1",
  // 一个提供实用函数的 JavaScript 库。  
  "lodash": "^4.17.21",
  // 在命令行中显示日志符号。  
  "log-symbols": "^4.1.0",
  // 创建可旋转的加载器  
  "ora": "5",
   // 估算操作进度。 
  "progress-estimator": "^0.3.1",
  // 一个特定于项目或定制的 CLI 工具  
  "pure-thin-cli": "^0.1.8",
    
  "rollup": "^4.6.1",
  "rollup-plugin-dts": "^5.3.0", // 是一个 Rollup 插件,它的主要作用是处理 TypeScript 的声明文件(.d.ts 文件)
  "rollup-plugin-esbuild": "^5.0.0",
  "rollup-plugin-node-externals": "^5.1.2", // 使rollup自动识别外部依赖
  "rollup-plugin-typescript2": "^0.36.0", // 支持rollup打包ts文件
    
  // 用于 Git 命令的 Node.js 封装。  
  "simple-git": "^3.19.1",
  // TypeScript 运行时库。  
  "tslib": "^2.6.1",
  "typescript": "^5.2.2"
},

目录结构如下,index.js:命令入口文件;command:命令逻辑;utils:公共方法

在这里插入图片描述

2. 配置打包命令

下载依赖

pnpm add -D rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-json rollup-plugin-typescript2 @rollup/plugin-terser rollup-plugin-node-externals

依赖说明:

  • rollup:打包工具,有很多选择,如webpack
  • @rollup/plugin-node-resolve:支持rollup打包node.js模块
  • @rollup/plugin-commonjs:支持rollup打包commonjs模块
  • @rollup/plugin-json:支持rollup打包json文件
  • rollup-plugin-typescript2:支持rollup打包ts文件
  • @rollup/plugin-terser:压缩打包代码
  • rollup-plugin-node-externals:使rollup自动识别外部依赖

根目录下新建 rollup.config.js

import { defineConfig } from 'rollup';
import nodeResolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import externals from "rollup-plugin-node-externals";
import json from "@rollup/plugin-json";
import terser from "@rollup/plugin-terser";
import typescript from 'rollup-plugin-typescript2';

export default defineConfig([
  {
    input: {
      index: 'src/index.ts', // 打包入口文件
    },
    output: [
      {
        dir: 'dist', // 输出目标文件夹
        format: 'cjs', // 输出 commonjs 文件
      }
    ],
    // 这些依赖的作用上文提到过
    plugins: [
      nodeResolve(),
      externals({
        devDeps: false, // 可以识别我们 package.json 中的依赖当作外部依赖处理 不会直接将其中引用的方法打包出来
      }),
      typescript(),
      json(),
      commonjs(),
      terser(),
    ],
  },
]);

package.json 中配置打包命令,-c 指定 rollup 配置文件,--bundleConfigAsCjs 将配置转为 commonjs 执行。

{
  ......
  "scripts": {
	......
    "build": "rollup -c rollup.config.js --bundleConfigAsCjs"
  },
}

index.ts加入一定代码后,执行 npm run build 测试打包结果

发现如下报错,这是因为默认生成的 tsconfig.jsonmoudlecommonjs,改为 'module: "ES2015"', 'module: "ES2020"', 'module: "ES2022"', or 'module: "ESNext"' 后即可。

在这里插入图片描述

在这里插入图片描述

再次执行 npm run build,打包配置完毕

在这里插入图片描述

3. 命令行交互配置依赖介绍

查看 create-react-app cli 的源码,发现其使用了如下依赖,我们也选择一部分安装

在这里插入图片描述

本文使用了如下依赖:

  • commander:解析命令行指令
  • ora:终端加载动画
  • progress-estimator:终端加载条动画
  • log-symbols:终端输出符号
  • chalk:终端字体美化
  • @inquirer/prompts:终端输入交互

其中最重要的是 commander,下载:pnpm install commander -D,用于解析用户在命令行输入的指令,可以到官方文档查看基本使用,并应用到 src/index.ts 中。

在这里插入图片描述

4. -v --version指令配置

src/index.ts 中尝试导入 Command 和 version 遇到如下报错

在这里插入图片描述

根据提示,将 tsconfig.json 中默认注释的 moduleResolution 放开即可,发现导入 package.json 仍报错,将 resolveJsonModule 取消注释。

在这里插入图片描述

在这里插入图片描述

继续完善 index.ts,自定义指令名称为 benchu,和 vite、vue-cli 一样就是一个命令名,添加 -v 或 --version 返回版本号,版本号为 package.json 中的 version 字段,该字段会在每次提交上传至 npm强制更新

import { Command } from "commander"
import { version } from "../package.json"

const program = new Command("benchu")
program.version(version, "-v, --version", "获取版本号")

program.parse()

打包后测试自定义命令 benchu,尝试查看版本与帮助说明

在这里插入图片描述

5. create 指令配置

声明创建命令,如使用 vue-cli 创建项目时输入的 vue create,可以让用户选择下载预设的模板,我们也命名一个 create 指令。

5.1 让用户输入项目名称 并 选择初始模版

  • src/command/create.ts 文件下编写 create 命令核心代码
  • 导出一个可以传入项目名称的方法,如果用户直接传入了项目名称则让用户选择模板,否则需要先让用户输入项目名称

create 后可以紧跟参数 name,代表项目名称,该参数为可选参数,可以只执行 create,后续步骤中要求用户输入项目名。

import { Command } from "commander"
import { version } from "../package.json"

const program = new Command("benchu")
program.version(version, "-v, --version", "版本号")

// create 指令
program
  .command("create")
  .description("初始化新项目")
  .argument("[name]", "项目名称") // "[name]"表示可选参数,"name"表示必填参数
  .action((dirName) => {
    console.log("init", dirName)
  })

program.parse()

重新打包,执行 npm run build 后,运行 dist/index.js 查看 command 此时已经可以看到 create 命令且正确拿到了 dirName

在这里插入图片描述

接下来处理 create 核心逻辑部分,首先在 src/index.ts 中将 dirName 交予 create

在这里插入图片描述

command/create.ts,首先需要确认预设模版对应远端仓库的关系,如本文连接到本人 gitee 的某个仓库,其中不同分支对应不同模版,可以供用户选择:

  • master:vite+ts+vue3+axios+pinia
  • element:vite+ts+vue3+axios+pinia+element+tailwind
  • element_layout:vite+ts+vue3+axios+pinia+element+tailwind+搭建完毕layout

@inquirer/prompts,可以帮助我们让用户在终端进行输入或选择的操作,本文使用到了 inputselect,更多使用方法可查阅官方文档:inquirer.js

select 要求数据格式如下:
在这里插入图片描述

import { select, input } from "@inquirer/prompts"
export interface TemplateInfo {
  name: string // 模板名称
  downloadUrl: string // 模板下载地址
  description: string // 模板描述
  branch: string // 模板分支
}

export const templates: Map<string, TemplateInfo> = new Map([
  [
    "Vite-Vue3-TypeScript-template",
    {
      name: "Vite-Vue3-TypeScript-template",
      downloadUrl: "[email protected]:tian__shuai/template-vite5--vue3.git",
      description: "vite + vue3 + ts初始模版",
      branch: "master",
    },
  ],
  [
    "Vite-Vue3-TypeScript-ElementUI-template",
    {
      name: "Vite-Vue3-TypeScript-ElementUI-template",
      downloadUrl: "[email protected]:tian__shuai/template-vite5--vue3.git",
      description: "vite + vue3 + ts + elementplus 初始模版",
      branch: "element",
    },
  ],
  [
    "Vite-Vue3-TypeScript-ElementUI-layout-template",
    {
      name: "Vite-Vue3-TypeScript-ElementUI-layout-template",
      downloadUrl: "[email protected]:tian__shuai/template-vite5--vue3.git",
      description: "vite + vue3 + ts + elementplus + layout 初始模版",
      branch: "element_layout",
    },
  ],
])

export async function create(projectName?: string) {
  if (!projectName) {
    projectName = await input({ message: "请输入项目名称" })
  }
  
  // 初始化模版列表
  const templateList = Array.from(templates).map(
    (item: [string, TemplateInfo]) => {
      const [name, info] = item
      return {
        name,
        value: name,
        description: info.description,
      }
    }
  )
  // 选了哪个模版
  const templateName = await select({
    message: "请选择模版",
    choices: templateList,
  })
  // 选中模版的详情
  const info = templates.get(templateName)
  console.log(info)
  console.log("create", projectName)
}

测试是否可以获取用户选择的模版详情,执行 npm run build => node dist/index.js create

在这里插入图片描述

输入项目名称后,通过上下键选择需要的模版,最下方就是定义的 description

在这里插入图片描述

选择后,获取到该模版的 info

在这里插入图片描述

create 时如果传递参数 name,则不会触发 input,而是直接进入 select 选择模版

在这里插入图片描述

5.2 下载选择的模版

新建 src/utils/clone.ts,用于处理下载模版,使用 simple-git 拉取 git 仓库,progress-estimator 设置预估 git clone 的时间并展示进度条。

参考 simple-git官方文档 ,git clone 需要传入三个参数:

  • url:仓库地址
  • localPath:目标路径
  • options:分支信息

在这里插入图片描述
src/utils/clone.ts,接收这三个参数:

import simpleGit from "simple-git"
export const clone = (url: string, projectName: string, options: string[]) => {}

将上一步 src/utils/create.ts 通过 select 拿到的模版 info 传入 src/utils/clone.ts 处理。

在这里插入图片描述

在项目根目录下创建 project 目录,用于存储下载的项目模版

在这里插入图片描述

完善 command/clone.ts

import { simpleGit, SimpleGit, SimpleGitOptions } from "simple-git"

const getOptions: Partial<SimpleGitOptions> = {
  baseDir: `${process.cwd()}/project`, // 指定 simple-git 操作的目录,默认为 process.cwd() 表示当前目录,我这里设置为根目录下的 project 目录,方便查看克隆多个项目的效果
  binary: "git", // 指定 git 的二进制文件位置
  maxConcurrentProcesses: 6, // 最大并发进程数
  trimmed: false, // git 输出的结果是否自动去除前后多余的空白字符
}

export const clone = async (
  url: string,
  projectName: string,
  branchOptions: string[]
) => {
  const git: SimpleGit = simpleGit(getOptions)
  try {
    await git.clone(url, projectName, branchOptions)
    console.log()
    console.log("代码下载完成!")
    console.log("=====================================================")
    console.log("================= 欢迎使用 benchu-cli ===============")
    console.log("=====================================================")
    console.log()
    console.log(
      "======== pnpm install 安装依赖, pnpm run dev 运行项目 ======="
    )
  } catch (e) {
    console.log("clone error", e)
  }
}

在这里插入图片描述

优化下载时的样式,使用 progress-estimator 添加进度条,progress-estimator官方文档地址

import { simpleGit, SimpleGit, SimpleGitOptions } from "simple-git"
import createLogger from "progress-estimator"

// 初始化进度条
const logger = createLogger({
  spinner: {
    interval: 100, // 100毫秒刷新一次
    frames: ["⠋", "⠙", "⠹", "⠸"], // 进度条的样式,
  },
})

const getOptions: Partial<SimpleGitOptions> = {
  baseDir: `${process.cwd()}/project`, // 指定 simple-git 操作的目录,默认为 process.cwd() 表示当前目录
  binary: "git", // 指定 git 的二进制文件位置
  maxConcurrentProcesses: 6, // 最大并发进程数
  trimmed: false, // git 输出的结果是否自动去除前后多余的空白字符
}

export const clone = async (
  url: string,
  projectName: string,
  branchOptions: string[]
) => {
  const git: SimpleGit = simpleGit(getOptions)
  try {
    await logger(git.clone(url, projectName, branchOptions), "代码下载中...", {
      estimate: 5000, // 预计下载时间
    })
    console.log()
    console.log("代码下载完成!")
    console.log("=====================================================")
    console.log("================= 欢迎使用 benchu-cli ===============")
    console.log("=====================================================")
    console.log()
    console.log(
      "======== pnpm install 安装依赖, pnpm run dev 运行项目 ======="
    )
  } catch (e) {
    console.log("clone error", e)
  }
}

效果:

在这里插入图片描述

继续优化样式,使用 chalk 添加颜色,chalk官方文档

在这里插入图片描述
src/command/clone.ts 完整代码

import { simpleGit, SimpleGit, SimpleGitOptions } from "simple-git"
import createLogger from "progress-estimator"
import chalk from "chalk"

// 初始化进度条
const logger = createLogger({
  spinner: {
    interval: 100, // 100毫秒刷新一次
    frames: ["⠋", "⠙", "⠹", "⠸"].map((item) => chalk.blue(item)), // 进度条的样式,
  },
})

const getOptions: Partial<SimpleGitOptions> = {
  baseDir: `${process.cwd()}/project`, // 指定 simple-git 操作的目录,默认为 process.cwd() 表示当前目录
  binary: "git", // 指定 git 的二进制文件位置
  maxConcurrentProcesses: 6, // 最大并发进程数
  trimmed: false, // git 输出的结果是否自动去除前后多余的空白字符
}

export const clone = async (
  url: string,
  projectName: string,
  branchOptions: string[]
) => {
  const git: SimpleGit = simpleGit(getOptions)
  try {
    await logger(git.clone(url, projectName, branchOptions), "代码下载中...", {
      estimate: 5000, // 预计下载时间
    })
    console.log()
    console.log(chalk.green("代码下载完成!"))
    console.log("=====================================================")
    console.log("================= 欢迎使用 benchu-cli ===============")
    console.log("=====================================================")
    console.log()
    console.log(
      "======== pnpm install 安装依赖, pnpm run dev 运行项目 ======="
    )
  } catch (e) {
    console.log("clone error", e)
  }
}

5.3 项目名相同检查是否需要覆盖更新

command/create.ts 中加入如下代码,判断项目名是否重复,并询问用户是否需要覆盖,如果覆盖,删除原有项目并让用户重新选择模版下载。

import path from "path"
import fs from "fs-extra"

// 省略其余代码 ...

// 是否覆盖同名项目
export function isOverwrite(projectName: string) {
  return select({
    message: "项目已存在,是否覆盖?",
    choices: [
      { name: "覆盖", value: true },
      { name: "不覆盖", value: false },
    ],
  })
}

export async function create(projectName?: string) {
  if (!projectName) {
    projectName = await input({ message: "请输入项目名称" })
  }
  
  // 判断是否覆盖同名项目
  const projectPath = path.resolve(`${process.cwd()}/project`, projectName) // 这里的路径保持和 clone.ts 中 simple-git 的 dirName 一致
  if (fs.existsSync(projectPath)) {
    const isRun = await isOverwrite(projectName)
    if (isRun) {
      await fs.remove(projectPath)
    } else {
      return
    }
  }

  const templateName = await select({
    message: "请选择模版",
    choices: templateList,
  })
  const info = templates.get(templateName)
	
  // 下载模版
  if (info) {
    clone(info.downloadUrl, projectName, ["-b", info.branch])
  }
}

选择覆盖后,原有的项目删除,选择新模版后重新下载

在这里插入图片描述

至此,一个功能极简的 脚手架工具 已经完成,下面需要考虑发布到 npm。

6. 发布至npm

发布前需要修改测试阶段创建的 src/project 目录,当时为了方便观察 create 效果,然而给用户使用时采用默认的 process.cwd() 即可。

在这里插入图片描述
在这里插入图片描述

6.1 README.md

需要添加 README.md 说明文档,分享一个在线生成图标网站,用于生成 npm 版本图标

在这里插入图片描述

也可以添加一些 icon,icon库地址

在这里插入图片描述

README.md

在这里插入图片描述

6.2 完善package.json

6.2.1 bin

补全 package.json,其中 bin 配置了命令 benchu,这个命令会映射到项目中的 bin/index.js ,即输入 benchu 就等于执行 bin/index.js,相当于测试阶段打包后执行 node dist/index.jsnpx benchu

在这里插入图片描述

在运行 benchu 命令时,实际上执行的脚本是 bin/index.js,需要确保这个文件具有正确的可执行权限,并在文件头部指定 Node.js 环境。

bin/index.js

#!/usr/bin/env node
require("../dist") // 执行编译后的文件 dist/index.js

6.2.2 files

files 字段在 package.json 中用于指定哪些文件或目录应包含在发布到 npm 注册表的包中,如果未设置 files 字段,npm 默认会包括除了 .gitignore.npmignore 文件中忽略的内容外的所有文件。

我这里只将 bin、dist、README.md 发布到npm。

在这里插入图片描述

6.3 发包

确认打包完成

检查 npm 源,如果是 淘宝源 则需要改回 npm 源。

查看npm镜像源地址

npm config get registry

我这里是淘宝镜像

在这里插入图片描述

切换到 npm 源

npm config set registry https://registry.npmjs.org/

登录 npm

npm login

发布

npm publish

注意: 每次 publish 都会强制要求更新 package.json 中的 version,可以手动修改,也可以通过命令 npm version patch,执行此命令前需要保证 Git 工作目录中没有未提交的更改或未跟踪的文件。

发布完毕后,即可在 npm 看到该包

在这里插入图片描述

可以看到 package.json 中 files 字段

在这里插入图片描述

7. 测试发布的包

全局下载 benchu-cli

npm install benchu-cli -g

执行命令

benchu create

8. 检查 npm 包的版本,并提示更新

benchu-cli 更新后需要给用户提示

command/create.ts 中,在检查是否需要覆盖项目后检查是否需要版本更新

安装 axios,get 获取 npm 包的详情

pnpm install axios -D
// ... 忽略其余包
import { name, version } from "../../package.json"
import axios from 'axios'

// 获取 npm 包的最新版本
const getNpmInfo = async (name: string) => {
  const npmUrl = `https://registry.npmjs.org/${name}`
  let res = {}
  try {
    res = await axios.get(npmUrl)
  } catch (e) {
    console.log(e)
  }
  return res
}
const getNpmLatestVersion = async (name: string) => {
  const { data } = (await getNpmInfo(name)) as AxiosResponse
  console.info(data)
}

// 检查版本
const checkVersion = async (name: string, version: string) => {
  const lastestVersion = await getNpmLatestVersion(name)
}


export async function create(projectName?: string) {
  // 项目名是否为空、是否需要覆盖项目 的逻辑...

  // 检查版本更新
  await checkVersion(name, version)

  // 提供模版选择、克隆仓库 的逻辑...
}

npm run build 重新打包并执行 node dist/index.js create,可以看到 npm 包的详情,其中 dist-tags 字段记录了最新版本。

在这里插入图片描述

继续优化,使用 lodashgt 比较版本号,并给出更新提示,我们也可以再定义一个命令 update 用于更新 benchu-cli。

// 用于检查 npm 包的版本,是否需要更新 npm 包
import { name, version } from "../../package.json"
import { gt } from "lodash"
import axios, { AxiosResponse } from "axios"
import chalk from "chalk"

// 获取 npm 包的最新版本
const getNpmInfo = async (cliName: string) => {
  const npmUrl = `https://registry.npmjs.org/${cliName}`
  let res = {}
  try {
    res = await axios.get(npmUrl)
  } catch (e) {
    console.log(e)
  }
  return res
}
const getNpmLatestVersion = async (cliName: string) => {
  const { data } = (await getNpmInfo(cliName)) as AxiosResponse
  return data["dist-tags"].latest
}

// 检查版本
export const checkVersion = async () => {
  const lastestVersion = await getNpmLatestVersion(name)
  const needUpdate = gt(lastestVersion, version)
  if (needUpdate) {
    console.warn(
      `${chalk.greenBright(name)} 版本需要更新,当前版本:${chalk.blueBright(
        version
      )}, 最新版本:${chalk.blueBright(lastestVersion)}`
    )
    console.log(
      `可使用${chalk.yellow(
        "npm install benchu-cli@latest -g"
      )}${chalk.yellow("benchu update")}更新`
    )
  }
}

手动修改 package.json 中的版本,再次执行 create 测试

在这里插入图片描述

9. update命令配置

上一步中通过比较 本地 与 npm 中 benchu-cli 的最新版本,给出用户提示,让其更新,我们也可以新增 update 命令用于更新包。

src/index.ts 新增 update 命令

import { Command } from "commander"
import { version } from "../package.json"
import { create } from "./command/create"
import { update } from "./command/update"

const program = new Command("benchu")
program.version(version, "-v, --version", "版本号")

// create 指令
program
  .command("create")
  .description("初始化新项目")
  .argument("[name]", "项目名称") // "[name]"表示可选参数,"name"表示必填参数
  .action((dirName) => {
    create(dirName) // 不考虑不传参的情况,统一交予 create 函数处理
  })

// update 指令
program
  .command("update")
  .description("更新 benchu-cli 工具")
  .action(async () => {
    await update()
  })

program.parse()

command/update.ts,通过 process 下载最新的包,child_process 是 Node.js 的核心模块之一,用于创建和管理子进程,以便运行系统命令、脚本或其他可执行文件。

import process from "child_process"
import chalk from "chalk"
import ora from "ora"

// 进度条
const spinner = ora({
  text: "benchu-cli 正在更新",
  spinner: {
    interval: 100, // 100毫秒刷新一次
    frames: ["⠋", "⠙", "⠹", "⠸"].map((item) => chalk.blue(item)), // 进度条的样式
  },
})

export const update = () => {
  spinner.start() // 开始动画
  process.exec("npm install benchu-cli@latest -g", (error, stdout) => {
    if (error) {
      spinner.fail()
      console.log(chalk.red(error))
    } else {
      spinner.succeed()
      console.log(chalk.green("更新成功"))
    }
  })
}

npm run build => node dist/index.js update

在这里插入图片描述

10. 优化终端打印样式

至此功能已经完全实现,但是控制台打印样式太丑了

在这里插入图片描述

utils/log.ts,使用 log-symbols 封装 log,log-symbols官方地址

下载


// 优化终端打印样式
import logSymbol from "log-symbols"

const log = {
  success: (message: string) => {
    console.log(logSymbol.success, message)
  },
  error: (message: string) => {
    console.log(logSymbol.error, message)
  },
  info: (message: string) => {
    console.log(logSymbol.info, message)
  },
  warn: (message: string) => {
    console.log(logSymbol.warning, message)
  },
}

export default log

原先的 clone.ts

在这里插入图片描述

使用 log 优化 clone.ts 打印

export const clone = async (
  url: string,
  projectName: string,
  branchOptions: string[]
) => {
  const git: SimpleGit = simpleGit(getOptions)
  try {
    await logger(git.clone(url, projectName, branchOptions), "代码下载中...", {
      estimate: 5000, // 预计下载时间
    })

    console.log()
    console.log(chalk.blackBright("======================================="))
    console.log(chalk.blackBright("========= 欢迎使用 benchu-cli ========="))
    console.log(chalk.blackBright("======================================="))
    console.log()

    log.success(`项目创建成功 ${chalk.blueBright(projectName)}`)
    log.success("执行以下命令启动项目")
    log.info(`cd ${chalk.blueBright(projectName)}`)
    log.info(`${chalk.yellow("pnpm")} install`)
    log.info(`${chalk.yellow("pnpm")} run dev`)
  } catch (e) {
    log.error(chalk.red("代码下载失败!"))
  }
}

在这里插入图片描述

继续优化,使用 figlet 添加打印效果,figlet官方文档。如这种效果:

在这里插入图片描述
安装 figlet 及其类型声明文件

pnpm install figlet @types/figlet

注意:要添加到生产依赖

在这里插入图片描述
clone.ts

const goodPrinter = async (message: string) => {
  const data = await figlet(message)
  console.log(chalk.rgb(40, 156, 193).visible(data))
}

在这里插入图片描述
在这里插入图片描述

;