(三)「造轮子」我也写了个Vue3脚手架!(命令行读取配置)

(一)「造轮子」我也写了个Vue3脚手架!(整体介绍)

(二)「造轮子」我也写了个Vue3脚手架!(项目环境搭建)

(三)「造轮子」我也写了个Vue3脚手架!(命令行读取配置)

本期介绍内容如下

使用commaner读取命令行参数
使用inquirer让用户选取对应配置
删除和生成项目目录

commander读取命令行参数

commander具体文档说明使用见这里

index.ts 复制代码
#! /usr/bin/env node
import packageJson from './package.json'
import { program } from 'commander'

interface Options {
  force?: boolean
}

program
  .name(packageJson.name)
  .description('cli to create a project of vue3')
  .version(packageJson.version)
program
  .command('create')
  .description('create a new project of vue3')
  .argument('<string>', 'project name')
  .option('-f, --force', 'overwirte target directory if it already exists')
  .action((name: string, options: Options) => {
    console.log(name, options)
  })
program.parse(process.argv)

我们使用的命令比较简单,用create xxx命令来创建名为xxx的项目;配置就使用-f--force用来在当前路径下已经存在xxx名字的目录时候直接强制删除

#! /usr/bin/env node这个标识系统应该使用 node 来执行这个脚本文件,咱们开发一个命令行工具这个当然需要带上

修改一下package.json的命令,这里调用create命令创建叫jqq的项目

package.json 复制代码
{
  "scripts": {
    "create": "npx esno ./index.ts create jqq",
  },
}

运行一下程序看下效果,也加上-f--force看看运行效果(这里用npm run create跑npm script的时候,要用--符号隔开在npx esno ./index.ts create jqq后面追加其他参数):

inquirer选取对应配置

我们在index.ts中,用一个 create 函数来写程序的主要创建流程,在 program 的 action 的回调函数中调用create函数

processTargetDirectory函数是询问用户是否需要对已经存在的目录进行覆盖的,inquireConfig是询问用户的配置的。这两个函数我们后面介绍

index.ts 复制代码
const create = async (name: string, options: Options) => {
  // 项目目录预处理
  await processTargetDirectory(name, options)

  // 询问用户需要的配置
  const { features } = await inquireConfig()
  const needsTypeScript = features.includes('typescript')
  const needsJsx = features.includes('jsx')
  const needsPrettier = features.includes('prettier')
  const needsRouter = features.includes('router')
  const needsPinia = features.includes('pinia')
  const needsVitest = features.includes('vitest')
  const needsEslint = features.includes('eslint')

  // 创建一下目录
  const targetDir = name
  const cwd = process.cwd()
  const root = path.join(cwd, targetDir)
  fs.mkdirSync(root)
}

processTargetDirectory

先看用户的force是否有命令行直接指定,没有指定并且已经有对应的目录,就询问用户是否要覆盖对应的目录。然后根据用户的选择来进行操作

    1. 有目录,不能强制覆盖,提示用户并且退出进程
    1. 有目录,可以强制覆盖,删除目录并提示删除成功

如下代码我们使用了 inquirer 让用户进行确认选择,chalk 就是让用户能够改变命令行的文本颜色,ora 用来做loading效果

index.ts 复制代码
// 目标目录可能存在,就要提醒用户是否进行删除
// 同时要配合用户的强制删除options,用户的配置优先级更高
const processTargetDirectory = async (name: string, options: Options) => {
  return new Promise(async (resolve) => {
    const cwd = process.cwd()
    const target = path.join(cwd, name)
    let force = options.force
    const isExistTarget = fs.existsSync(target)
    // 有目录,无force配置时候才询问用户
    if (isExistTarget && typeof options.force === 'undefined') {
      // 询问用户
      const answer = await inquirer.prompt<{ force: boolean }>([
        {
          type: 'confirm',
          name: 'force',
          message: `Do you want to overwrite directory ${name}`,
          default: false,
        },
      ])
      force = answer.force
    }
    // 有目录,不能强制覆盖
    if (isExistTarget && !force) {
      console.log(chalk.red('✖') + ' ' + `The directory ${name} already exists`)
      process.exit(0)
    }
    // 有目录,可以强制覆盖
    if (isExistTarget && force) {
      const spinner = ora(`${name} is deleting`)
      spinner.start()
      fs.remove(target, (err) => {
        if (err) {
          console.error(err)
          process.exit(0)
        }
        spinner.succeed(`delete ${name} success`)
        return resolve(true)
      })
    } else {
      return resolve(true)
    }
  })
}

inquireConfig

我们未来会支持的配置见 FEATURE_OPTIONS,这一期先能让用户选择这些配置

这里代码也比较简单,直接调用inquirer的方法给出个命令行的多选框让用户进行选择,用户选择返回的是一个字符串数组

index.ts 复制代码
const FEATURE_OPTIONS = [
  {
    value: 'typescript',
    name: 'TypeScript',
  },
  {
    value: 'jsx',
    name: 'JSX 支持',
  },
  {
    value: 'router',
    name: 'Router(单页面应用开发)',
  },
  {
    value: 'pinia',
    name: 'Pinia(状态管理)',
  },
  {
    value: 'vitest',
    name: 'Vitest(单元测试)',
  },
  {
    value: 'eslint',
    name: 'ESLint(错误预防)',
  },
  {
    value: 'prettier',
    name: 'Prettier(代码格式化)',
  },
] as const
type Feature = (typeof FEATURE_OPTIONS)[number]['value'][]

const inquireConfig = async () => {
  const answer = await inquirer.prompt<{ features: Feature }>([
    {
      type: 'checkbox',
      name: 'features',
      message: 'Please select the features',
      choices: FEATURE_OPTIONS,
    },
  ])
  return answer
}

代码运行测试

这里给出部分运行情况的结果,其他情况可以自行测试

  • 没有文件夹的时候

  • 已经存在文件夹的时候

  • 配合-f

本期最终代码

index.ts 复制代码
#! /usr/bin/env node
import packageJson from './package.json'
import { program } from 'commander'
import fs from 'fs-extra'
import inquirer from 'inquirer'
import ora from 'ora'
import chalk from 'chalk'
import path from 'path'

interface Options {
  force?: boolean
}

// 目标目录可能存在,就要提醒用户是否进行删除
// 同时要配合用户的强制删除options,用户的配置优先级更高
const processTargetDirectory = async (name: string, options: Options) => {
  return new Promise(async (resolve) => {
    const cwd = process.cwd()
    const target = path.join(cwd, name)
    let force = options.force
    const isExistTarget = fs.existsSync(target)
    // 有目录,无force配置时候才询问用户
    if (isExistTarget && typeof options.force === 'undefined') {
      // 询问用户
      const answer = await inquirer.prompt<{ force: boolean }>([
        {
          type: 'confirm',
          name: 'force',
          message: `Do you want to overwrite directory ${name}`,
          default: false,
        },
      ])
      force = answer.force
    }
    // 有目录,不能强制覆盖
    if (isExistTarget && !force) {
      console.log(chalk.red('✖') + ' ' + `The directory ${name} already exists`)
      process.exit(0)
    }
    // 有目录,可以强制覆盖
    if (isExistTarget && force) {
      const spinner = ora(`${name} is deleting`)
      spinner.start()
      fs.remove(target, (err) => {
        if (err) {
          console.error(err)
          process.exit(0)
        }
        spinner.succeed(`delete ${name} success`)
        return resolve(true)
      })
    } else {
      return resolve(true)
    }
  })
}

const FEATURE_OPTIONS = [
  {
    value: 'typescript',
    name: 'TypeScript',
  },
  {
    value: 'jsx',
    name: 'JSX 支持',
  },
  {
    value: 'router',
    name: 'Router(单页面应用开发)',
  },
  {
    value: 'pinia',
    name: 'Pinia(状态管理)',
  },
  {
    value: 'vitest',
    name: 'Vitest(单元测试)',
  },
  {
    value: 'eslint',
    name: 'ESLint(错误预防)',
  },
  {
    value: 'prettier',
    name: 'Prettier(代码格式化)',
  },
] as const
type Feature = (typeof FEATURE_OPTIONS)[number]['value'][]

const inquireConfig = async () => {
  const answer = await inquirer.prompt<{ features: Feature }>([
    {
      type: 'checkbox',
      name: 'features',
      message: 'Please select the features',
      choices: FEATURE_OPTIONS,
    },
  ])
  return answer
}

const create = async (name: string, options: Options) => {
  // 项目目录预处理
  await processTargetDirectory(name, options)

  // 询问用户需要的配置
  const { features } = await inquireConfig()
  const needsTypeScript = features.includes('typescript')
  const needsJsx = features.includes('jsx')
  const needsPrettier = features.includes('prettier')
  const needsRouter = features.includes('router')
  const needsPinia = features.includes('pinia')
  const needsVitest = features.includes('vitest')
  const needsEslint = features.includes('eslint')

  // 创建一下目录
  const targetDir = name
  const cwd = process.cwd()
  const root = path.join(cwd, targetDir)
  fs.mkdirSync(root)
}

program
  .name(packageJson.name)
  .description('cli to create a project of vue3')
  .version(packageJson.version)
program
  .command('create')
  .description('create a new project of vue3')
  .argument('<string>', 'project name')
  .option('-f, --force', 'overwirte target directory if it already exists')
  .action((name: string, options: Options) => {
    create(name, options)
  })
program.parse(process.argv)
package.json 复制代码
{
  "name": "create-jqq",
  "version": "0.0.0",
  "type": "module",
  "bin": {
    "create-jqq": "./index.js"
  },
  "scripts": {
    "create": "npx esno ./index.ts create jqq",
    "format": "prettier --write .",
    "prepare": "husky"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "devDependencies": {
    "@types/ejs": "^3.1.5",
    "@types/fs-extra": "^11.0.4",
    "@types/lodash-es": "^4.17.12",
    "@types/node": "^22.14.0",
    "chalk": "^5.4.1",
    "commander": "^13.1.0",
    "ejs": "^3.1.10",
    "esno": "^4.8.0",
    "fs-extra": "^11.3.0",
    "husky": "^9.1.7",
    "inquirer": "^12.5.2",
    "lint-staged": "^15.5.1",
    "lodash-es": "^4.17.21",
    "ora": "^8.2.0",
    "prettier": "3.5.3"
  },
  "lint-staged": {
    "*.{js,ts,vue,json}": [
      "prettier --write"
    ]
  }
}

下期预告

下期我们就开始讲如何进行模板文件的拆分和组织,主要是这个核心思路的处理

相关推荐
BillKu9 分钟前
Vue3 + TypeScript中provide和inject的用法示例
javascript·vue.js·typescript
培根芝士15 分钟前
Electron打包支持多语言
前端·javascript·electron
Baoing_41 分钟前
Next.js项目生成sitemap.xml站点地图
xml·开发语言·javascript
mr_cmx41 分钟前
Nodejs数据库单一连接模式和连接池模式的概述及写法
前端·数据库·node.js
a东方青1 小时前
vue3学习笔记之属性绑定
vue.js·笔记·学习
东部欧安时1 小时前
研一自救指南 - 07. CSS面向面试学习
前端·css
沉默是金~1 小时前
Vue+Notification 自定义消息通知组件 支持数据分页 实时更新
javascript·vue.js·elementui
涵信1 小时前
第十二节:原理深挖-React Fiber架构核心思想
前端·react.js·架构
在下千玦1 小时前
#去除知乎中“盐选”付费故事
javascript
ohMyGod_1231 小时前
React-useRef
前端·javascript·react.js