用typescript开发的cli工具

564 阅读4分钟

pander开发总结

一、背景

好奇于vite项目的脚手架工具,一直想自己也造个工具,帮助平常学习。

github 项目地址

npm包地址

个人博客

二、问题

本来想整个jest进行一下测试,写到后面发现笑死,根本不会。

就简单用ts搭建了开发环境。

在开发过程中,遇到了如下问题

  1. 依赖版本问题
  2. json导入问题
  3. __dirname问题

下面来一一阐述如何解决以上问题

三、解决问题

1.依赖版本问题

由于现在esmodule全面普及,很多npm包也跟着更新,例如在开发过程中用到的chalk、ora等包,高版本仅支持es模块导入方式,不支持commonjs导入方式。

虽然可以降低版本,但也意味着不能体验新特性,始终不是解决问题的较好方式,这些模块的更新,也意味着未来将会是es模块导入方式的天下。

下面是我解决的方法

1.在package.json文件加入type:"module"

{
  //...
  "type": "module"
}

2.json导入问题

当修改模块引入方式后,随之而来的是json不能正常导入了。原因是es模块导入json文件得加个断言,如下所示

import template from '../template.json' assert {type:'json'};

3.__dirname问题

同样造成的问题是__dirname不能正常使用了,原因是这是commonjs模块特有的变量

通过下述方式可以解决这个问题

es模块有元信息import.meta.url可以通过这个进行一些处理就能获得__dirname

import {dirname} from 'path'
import {fileURLToPath} from 'url'
//将元信息的url转换一下
const __filename=fileURLToPath(import.meta.url);
//获取__dirname,通过path模块的dirname截取文件夹路径
const __dirname=dirname(__filename)

4.pnpm踩坑

得通过pnpm link --global才能链接到全局,我本来以为跟npm一样。

5.待解决问题

由于学的不精,还没尝试过给包编写声明文件,所以引入的时候,我直接忽略了该模块的类型

四、项目代码

这个项目逻辑并不难,相信大家有手就行,具体逻辑就不再慢慢阐述,直接上代码

目录结构

image-20220711183538197

入口文件

src目录下index.ts

#!/usr/bin/env node
import {Command} from 'commander'
import {clear} from './commands/clear.js'
import {add} from './commands/add.js'
import { list } from './commands/list.js'
import { del } from './commands/delete.js'
import { create } from './commands/create.js'
import { outFile } from './commands/export.js'
import { importFile } from './commands/import.js'
const program=new Command()
program.usage('<command>')

program.version('1.0.0')

program.command('add')
  .description('add a new template (添加一个新模板)')
  .action(()=>{
    add()
  })

program.command('list')
  .description('list the templates')
  .action(()=>{
    list()
  })

program.command('clear')
  .description('delete all of the template')
  .action(()=>{
    clear()
  })

program.command('delete')
  .description('delete template')
  .action(()=>{
    del()
  })

program.option("-d,--del","delete template")
  .action(()=>{
    del()
  })

program.command('create')
  .description('to create a project')
  .action(()=>{
    create()
  })

program.command('export')
  .description('export you templates into template.json')
  .action(()=>{
    outFile()
  })

program.command('import')
  .description('import you templates from template.json')
  .action(()=>{
    importFile()
  })
//这个必须放最后
program.parse(process.argv)

工具封装

utils下utils.ts文件

import inquirer from "inquirer"
import fs from 'fs'
import Table from 'cli-table'
import {dirname} from 'path'
import {fileURLToPath} from 'url'
export type Reset<T extends Record<string,string>>=(keyof T)[]
//处理字符串两边空格
export function trim(str:string){
  return str.trim()
}
//询问方法
export type Options=inquirer.QuestionCollection<inquirer.Answers>
export async function ques(opts:Options){
  const answer=await inquirer.prompt(opts)
  return answer
}
//写入文件
export type TmpData=Record<string,string>
export function write(url:string,data:TmpData){
  const str=JSON.stringify(data,null,'\t')
  fs.writeFileSync(url,str,'utf-8')
}
//打印表格
const table=new Table({
  head:['name','url'],
  style:{
    head:['green'],
    border:['yellow'],
  }
})
export function showTable(tempList:Record<string,string>){
  const list=Object.entries(tempList)
  if(list.length>0){
    table.push(...list)
    console.log(table.toString())
    process.exit()
  }else{
    console.log(table.toString())
    process.exit()
  }
}

export function getDirname(url:string){
  return dirname(fileURLToPath(url))
}

命令模块

commander目录由于文件太多,就挑几个有代表性的展示

add.ts

#!/usr/bin/env node
import template  from '../template.json' assert {type:'json'};
import {trim,ques,Options,write,getDirname} from '../utils/util.js'
import {resolve} from 'path'
import { showTable } from '../utils/util.js'
import logSymbols from 'log-symbols'
import chalk from 'chalk'
const tmpUrl=resolve(getDirname(import.meta.url),'../template.json')

const opts:Options=[
  {
    name:'name',
    type:'input',
    message:'请输入模板名称',
    validate:(input:string)=>{
      if(trim(input).length===0){
        return 'name is required'
      }else if((template as any)[input]){
        return 'name han been used'
      }else{
        return true
      }
    }
  },
  {
    name:'url',
    type:'input',
    message:'请输入模板远程仓库地址',
    validate:(input:string)=> {
      if(trim(input).length===0){
        return 'url is required'
      }else{
        return true
      }
    },
  }
]

export async function add(options:Options=opts){
  let {name,url}=await ques(options)
  name=trim(name)
  url=trim(url)
  const data:any={
    ...template,
  }
  data[name]=url.replace(/[\u0000-\u0019]/g, '') // 过滤 unicode 字符
  write(tmpUrl,data)
  console.log('\n')
  console.log(chalk.greenBright(logSymbols.success),chalk.greenBright('Add a template successfully!'))
  console.log(chalk.greenBright('The latest templateList is: \n'))
  showTable(data)
}

delete.ts

import template from '../template.json' assert {type:'json'};
import { write ,Options,ques,getDirname,Reset} from '../utils/util.js';
import { resolve } from "path";
import chalk from 'chalk';
import logSymbols from 'log-symbols';
const tmpUrl=resolve(getDirname(import.meta.url),'../template.json')

const choices=Object.keys(template)

const opts:Options={
  name:'templates',
  type:'checkbox',
  message:'choice the templates that which you want to delete',
  choices,
}

export async function del(){
  if(choices.length==0){
    console.log(chalk.yellowBright(logSymbols.info,'the templates has been empty!'))
    process.exit()
  }
  const res:{
    templates:string[]
  }=await ques(opts) as any
  //获得剩下的
  const surplus:Reset<typeof template>=choices.filter(x=>{
    return !res.templates.includes(x) 
  }) as any
  //把剩下的保存起来
  const data:Record<string,string>={}
  for(let i of surplus){
    data[i]=template[i]
  }
  //重写回文件
  write(tmpUrl,data)
  console.log(chalk.greenBright(logSymbols.success+' delete successfully\n'))
  console.log(chalk.greenBright(logSymbols.info+" use 'pander list' to show list" ))
}

create.ts

import ora from "ora";
import template from '../template.json' assert {type:'json'}
import logSymbols from "log-symbols";
import chalk from "chalk";
import {Options,ques,Reset} from '../utils/util.js'
//@ts-ignore
import download from 'download-git-repo'
chalk.level=1
const choices:Reset<typeof template>=Object.keys(template) as any
const opts:Options=[
  {
    name:'template',
    type:'list',
    message:'choice which template to create project',
    choices,
  },
  {
    name:'dirname',
    type:'input',
    message:'please input you project name',
    default:'my-project'
  }
]

export async function create(){
  const res:{template:typeof choices[number],dirname:string}=await ques(opts) as any
  console.log(chalk.blueBright('\n Start creating ... \n'))
  const spinner=ora("Downloading...");
  spinner.start();
  download(`direct:${template[res.template]}`,`./${res.dirname}`,{clone:true},(err:any)=>{
    if(err){
      spinner.fail();
      console.log(chalk.red(logSymbols.error+` Create failed. ${err}`))
      return
    }

    //结束加载图标
    spinner.succeed();
    console.log(chalk.greenBright(logSymbols.success+' Create completed!'))
    console.log(`\n cd ${res.dirname}`)
    console.log('\n npm i \n')
  })
}

谢谢观看,觉得还行就点个赞吧,你们的点赞是我学习的动力