Published on

从0搭建自己的CLI

背景

  • 自己开发玩具项目时 会频繁做一些重复的工作(项目初始化、配置),浪费时间
  • 需要一些基础功能、如路由、eslint 等

所以有了设计一个快速生成项目模板脚手架的想法,有更多精力去 coding

调研

参考自己最常使用的工具 vite,给用户提供一些可交互的选项,然后生成项目模板

提供了几个基础选项(自己比较常用)

  • router
  • eslint
  • cssinjs
  • ...

开发

实现的大致思想是 配置 + 模版

通过用户选择的配置,结合项目中的模板,快速生成项目

prompts库命令行交互,获取用户需要的模块

const result = await prompts(
  [
    // name
    {
      type: 'text',
      name: 'projectName',
      message: reset('Project name:'),
      initial: defaultProjectName,
    },
    // modules
    {
      type: 'multiselect',
      name: 'modules',
      message: 'Select the required modules',
      instructions: false,
      choices: [
        { title: 'router', value: 'router' },
        { title: 'eslint', value: 'eslint' },
        { title: 'css in js', value: 'cssinjs' },
      ],
    },
  ],
  {
    onCancel: () => {
      throw new Error(red('✖') + ' Operation cancelled')
    },
  }
)

获取其模块对应的依赖,

export type PackageParam = Partial<{
  dependencies?: Record<string, string>
  devDependencies?: Record<string, string>
  scripts: Record<string, string>
}>

const deps: PackageParam = {
  eslint: {
    devDependencies: {
      'vite-plugin-eslint': '^1.8.1',
      eslint: '^8.31.0',
      '@mmc-cloud/eslint-config': '^1.0.4',
    },
    scripts: {
      fix: 'eslint --ext .js,.jsx,.ts,.tsx --fix --quiet ./',
    },
  },
}

动态生成一个 package.json 文件, 实现如下

// 获取对应依赖
const getDeps = (deps: string[]) =>
  deps.reduce<Omit<PackageParam, 'name'>>((pre, cur) => {
    const { dependencies, devDependencies, scripts } = depsCharset[cur] ?? {}

    pre.dependencies = { ...pre.dependencies, ...dependencies }
    pre.devDependencies = { ...pre.devDependencies, ...devDependencies }
    pre.scripts = { ...pre.scripts, ...scripts }

    return pre
  }, {})

// 生成 package.json
const generatePackage = (param: PackageParam) => {
  const { name, dependencies, devDependencies, scripts } = param
  const template: Record<string, unknown> = {
    name,
    private: true,
    version: '0.0.0',
    type: 'module',
    scripts: {
      // dev ...
      ...scripts,
    },
    dependencies: {
      // react ...
      ...dependencies,
    },
    devDependencies: {
      // ts ...
      ...devDependencies,
    },
  }

  return template
}

const deps = getDeps(modules as string[])

const packageJson = generatePackage({ name: projectName, ...deps })

这样我们就可以动态生成一个package.json文件

再通过基础模版 + 用户选择的模版,来生成最终项目

新建一个 template文件夹,下面存放一些模块模版

  • base 基础模版
  • eslint
  • router
  • ...
for (const module of ['base', ...modules]) {
  const template = `${templateDir}/${module}`
  const files = readdirSync(template)
  for (const file of files) {
    copy(resolve(template, file), resolve(projectDir, file))
  }
}

在把对应模板拷贝到项目目录下

拓展

  • deps 依赖表
  • template 模块对应依赖

维护以上两个目录,可以实现自定义拓展

启动

package.json文件下添加

  "bin": {
    "create-mmc": "index.js"
  },

index.js

最上面一行注释不要删除

#!/usr/bin/env node

import './dist/index.mjs'

再执行npm link命令

最后可以在命令行执行 create-mmc

总结

还不够完善,后续可以实现更多模板内容。

初步了解了 cli 工具的搭建

项目地址