CLI
命令行界面:Command Line Interface
大体流程
1. 创建bin文件夹,创建一个文件hsh(没有后缀名)
js
#!/usr/bin/env node
import { runCLI } from '../dist/index.js'
runCLI()2. package.json
json
{
"bin": {
"hsh": "bin/hsh"
}
}3. 创建index.ts和cli.ts
ts
import { run } from './cli.js'
export const defineConfig = () => {}
export const runCLI = () => {
run(process.argv)
}ts
import { program } from 'commander'
import './commands/index.js'
import './utils/loadTemplate.js'
program.version('0.0.1').name('hsh')
export const run = (args: string[]) => {
program.parse(args)
}4. 配置tsup.config.ts (前提:npm install tsup)
ts
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['./src/index.ts'],
format: ['esm'],
shims: true,
clean: true,
dts: true,
platform: 'node',
outDir: 'dist'
})将index.ts文件转译成/dist/index.js
5. 设置tsup
json
{
"scripts": {
"dev": "tsup --watch",
"build": "tsup"
}
}pnpm dev改动代码实时更新,pnpm build启动tsup
6. 创建src/commands/base,里面创建如下文件
ts
import { Command } from 'commander'
import { logger } from '../../utils/logger.js'
import { spawn } from 'node:child_process'
export function build(program: Command) {
return program
.createCommand('build')
.description('build project')
.action(() => {
// 执行项目的打包命令
// npm run build
logger.info('build projects')
const command = 'npm'
const params = ['run', 'build']
const child = spawn(command, params, {
stdio: 'inherit',
shell: process.platform === 'win32' // 在 windows 系统下开启 shell 模式以正确识别 npm.cmd
})
child.on('close', (code) => {
logger.log(`子进程退出,退出码:${code}`)
})
})
}ts
import { Command } from 'commander'
import { logger } from '../../utils/logger.js'
import picocolors from 'picocolors'
import { loadTemplate } from '../../utils/loadTemplate.js'
import prompts from 'prompts'
export function create(program: Command) {
return program
.createCommand('create')
.description('create project')
.arguments('<name>')
.option('-t, --template <template>', 'template name')
.action(async (projectName, options) => {
logger.log('创建项目')
logger.info(
`create projects, name:${projectName}, ${JSON.stringify(options)}`
)
let { template } = options
if (!template) {
const { framework } = await prompts({
type: 'select',
name: 'framework',
message: 'please select a framework',
choices: [
{ title: 'react', value: 'react' },
{ title: 'vue', value: 'vue' },
{ title: 'angular', value: 'angular' }
]
})
template = framework
}
// 选择模板 本地 或者 远程
const { source } = await prompts({
type: 'select',
name: 'source',
message: 'please select local or remote template',
choices: [
{ title: 'local', value: 'local' },
{ title: 'remote', value: 'remote' }
]
})
if (!source) return
logger.info(picocolors.bgCyan(`create ${template} project`))
// react 不是写死的 要用户选择 项目名称也要用户输入
loadTemplate({
projectName,
templateName: template,
local: source === 'local'
})
})
}ts
import { Command } from 'commander'
import { logger } from '../../utils/logger.js'
import { spawn } from 'node:child_process'
export function preview(program: Command) {
return program
.createCommand('preview')
.description('preview project')
.action(() => {
// 执行项目的打包命令
// npm run preview
logger.info('preview projects')
const command = 'npm'
const params = ['run', 'preview']
const child = spawn(command, params, {
stdio: 'inherit',
shell: process.platform === 'win32' // 在 windows 系统下开启 shell 模式以正确识别 npm.cmd
})
child.on('close', (code) => {
logger.log(`子进程退出,退出码:${code}`)
})
})
}ts
import { Command } from 'commander'
import { logger } from '../../utils/logger.js'
import { spawn } from 'node:child_process'
export function serve(program: Command) {
return program
.createCommand('serve')
.description('serve project')
.action(() => {
// npm run dev pnpm dev
logger.log('启动项目')
/**
* node 中怎么执行命令
*/
const command = 'npm'
const params = ['run', 'dev']
const child = spawn(command, params, {
stdio: 'inherit',
shell: process.platform === 'win32' // 在 windows 系统下开启 shell 模式以正确识别 npm.cmd
})
child.on('close', (code) => {
logger.log(`子进程退出,退出码L: ${code}`)
})
})
}ts
// ESM
import { logger } from '../../utils/logger.js'
import pkg from '../../../package.json'
import picocolors from 'picocolors'
import { Command } from 'commander'
export function info(program: Command) {
return program
.createCommand('info')
.description('show info')
.action(() => {
logger.info('Using consola 3.0.0')
logger.start('Building project...')
logger.warn('A new version of consola is available: 3.0.1')
logger.success('Project built!')
// logger.error(new Error('This is an example error. Everything is fine!'))
logger.box('huangshuheng')
logger.log(picocolors.bgGreen(`Product: hsh CLI v${pkg.version}`))
logger.log(picocolors.green(`Author: hsh`))
logger.log(picocolors.underline(`License: ${pkg.license}`))
})
}7. 在commands文件夹下创建如下
ts
import { create } from './base/create.js'
import { registerCommand } from './registerCommand.js'
import { info } from './base/info.js'
import { serve } from './base/serve.js'
import { build } from './base/build.js'
import { greet } from './base/greet.js'
import { preview } from './base/preview.js'
// 注册命令
registerCommand(create)
registerCommand(info)
registerCommand(serve)
registerCommand(build)
registerCommand(greet)
registerCommand(preview)ts
import { Command, program } from 'commander'
type Fn = (p: Command) => Command
// 这个类型是一个 函数类型 传入的参数是 Command类型 返回值也是Command类型
/**
* 注册命令
* @param fn1 命令函数
*/
export function registerCommand(fn1: Fn) {
program.addCommand(fn1(program))
}8. 在src文件夹下创建utils文件夹,在utils文件夹创建文件如下
ts
import { copy } from 'fs-extra'
import { join } from 'node:path'
import { downloadTemplate } from 'giget'
interface LoadTemplateParams {
projectName: string
templateName: string
local?: boolean
}
/**
* 加载本地模板
*/
const loadLocalTemplate = async (params: LoadTemplateParams) => {
// 把模板 复制到 当前文件夹下去
copy(
join(__dirname, `../templates/template-${params.templateName}`),
`${process.cwd()}/${params.projectName}`
)
// process.cwd() 返回当前的工作目录
}
/**
* 加载远程模板
* @param params
*/
const loadRemoteTemplate = async (params: LoadTemplateParams) => {
const { dir } = await downloadTemplate(
// giget 尝试解压文件 所以链接必须是一个压缩包的地址
// 改用gh前缀 会触发专门的api去下载 .tar.gz 存档
'gh:huangshuheng0405/javascript#main',
{
dir: `${process.cwd()}/.temp`,
force: true
// 确保 .temp 目录中存在之前下载失败的残留文件 会被强制覆盖 保证下载环境干净
}
)
await copy(dir, `${process.cwd()}/${params.projectName}`)
}
export const loadTemplate = async (params: LoadTemplateParams) => {
const { local } = params
if (local) {
await loadLocalTemplate(params)
} else {
await loadRemoteTemplate(params)
}
}ts
import { createConsola } from 'consola'
export const logger = createConsola()9. 大致如上,其余详情见cli
files
在package.json中 files字段作用是定义在执行npm publish发布包时,那些文件或目录应该被上传到npm仓库
- 默认情况,
npm会上传全部文件,通过设置files可以排除掉源代码(src)、测试文件(test)、配置文件(ts.config.json)等开发环境才需要的文件,从而大幅减少用户安装你的cli工具时的下载体积
npm包发布
在cli文件夹下的package.json设置
json
{
"scripts": {
"publish:npm": "npm publish --access public"
}
}先运行npm login登录npm,再运行pnpm publish:npm上传即可,版本号必须和上次版本号大