介绍
pander开发总结,pander是我于2022年7月8日开发完成的一个模板管理工具
# pander开发总结
# 一、背景
好奇于vite项目的脚手架工具,一直想自己也造个工具,帮助平常学习。
github 项目地址 (opens new window)
# 二、问题
本来想整个jest进行一下测试,写到后面发现笑死,根本不会。
就简单用ts搭建了开发环境。
在开发过程中,遇到了如下问题
- 依赖版本问题
- json导入问题
- __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.待解决问题
由于学的不精,还没尝试过给包编写声明文件,所以引入的时候,我直接忽略了该模块的类型
# 四、项目代码
这个项目逻辑并不难,相信大家有手就行,具体逻辑就不再慢慢阐述,直接上代码
# 目录结构
# 入口文件
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')
})
}