Node Cli 常用工具介绍

仓库地址

新手教程

项目目录

1
2
3
4
5
|-- node-cli
|-- .gitignore // git 忽略文件
|-- index.js // 入口文件
|-- package.json // 项目配置
|-- README.md // 项目说明

配置项目

  • 初始化项目

    1
    npm init -y
  • 修改 package.json 文件内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    "name": "create-yourname-app", // 项目名称 npm i create-yourname-app -g
    "version": "1.0.0",
    "description": "",
    "author": "",
    "license": "MIT",
    "main": "index.js",
    "files": ["index.js"], // 需要发布的文件
    "bin": {
    "create-yourname-app": "./index.js" // 全局命令 create-yourname-app (重要)
    },
    "keywords": [],
    "scripts": {
    "dev": "node ./index.js" // 监听文件变化
    }
    }
  • 修改 index.js 文件内容如下

    1
    2
    3
    4
    // 标记为可执行文件
    #! /usr/bin/env node

    console.log("Hello, World!");

项目运行

  • 本地运行

    1
    npm run dev
  • 本地模仿全局命令

    1
    2
    3
    4
    5
    6
    # 在本地全局包中生成一个软连接指向当前目录
    # 查看本地全局包 npm ls -g
    npm link

    # 执行 bin 命令
    create-yourname-app
  • 上传 npm

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 上传包
    npm login
    npm publish

    # 全局安装
    npm install create-yourname-app -g

    # 执行 bin 命令
    create-yourname-app

项目实践

项目目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|-- node-cli
|-- dist // 打包后的文件
|-- src // 源码目录
| |-- core // 核心代码
| | |-- index.ts // 核心代码入口文件
| | |-- registerException.ts // 异常处理
| | |-- registerFiglet.ts // ASCII 艺术字
| | |-- registerPrompts.ts // 命令交互逻辑
| |-- utils // 工具函数
| | |-- index.ts // 工具函数入口文件
| | |-- handleCheckFolder.ts // 检测文件夹是否为空
| | |-- handleDownloadZip.ts // 下载 zip 文件
| | |-- handleGitBranches.ts // 获取远程仓库的所有分支名
| |-- index.ts // 入口文件
|-- templates // 模板目录
|-- .gitignore // git 忽略文件
|-- index.js // 入口文件
|-- package.json // 项目配置
|-- README.md // 项目说明
|-- tsconfig.json // TypeScript 配置文件

配置项目

  • 初始化项目

    1
    npm init -y
  • 修改 package.json 文件内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    {
    "name": "@zxiaosi/cli", // 项目名称 npm i @zxiaosi/cli -g
    "version": "1.0.0",
    "description": "",
    "author": "",
    "license": "MIT",
    "main": "index.js",
    "files": ["src", "dist", "templates", "index.js"], // 需要发布的文件
    "bin": {
    "zxiaosi": "./index.js" // 全局命令 zxiaosi 重要
    },
    "keywords": [],
    "publishConfig": {
    "access": "public"
    },
    "dependencies": {},
    "devDependencies": {
    "typescript": "^5.7.3"
    },
    "scripts": {
    "clean": "rimraf ./node_modules",
    "dev": "tsc -w", // 监听文件变化
    "build": "rimraf ./dist && tsc", // 打包
    "preview": "node ./index.js", // 本地预览
    "publish": "npm run build && npm publish"
    }
    }
  • 修改 index.js 文件内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    // 标记为可执行文件
    #! /usr/bin/env node

    // 引入 dist 中的入口文件
    const CLI = require('./dist/index').default;

    // 运行程序
    new CLI().run();

配置 TypeScript 打包文件

  • 安装 TypeScript

    1
    npm install --save-dev typescript
  • 初始化 TypeScript 配置文件

    1
    npx tsc --init
  • 修改 tsconfig.json 文件内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    {
    "compilerOptions": {
    "target": "ES5",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "declaration": true,
    "outDir": "./dist"
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules"]
    }

配置 figlet 展示ASCII 文艺字 (可选)

  • 安装 figlet

    1
    2
    3
    4
    5
    npm install figlet
    npm i @types/figlet -D

    // 控制台输出样式
    npm i picocolors
  • 修改 src/core/registerFiglet.ts 文件内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import figlet from 'figlet';
    import pc from 'picocolors';
    import config from '../config';

    /**
    * ASCII 艺术字
    */
    export default function () {
    const figletText = figlet.textSync('zxiaosi', {
    horizontalLayout: 'full',
    });
    console.log(pc.green(figletText));
    }
  • src/index.ts 中添加 registerFiglet 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import { registerFiglet } from './core/registerFiglet';

    export default class CLI {
    constructor(appPath: any) {
    registerFiglet();
    }

    run() {}
    }
  • 效果如下

配置 axios 获取远程仓库分支

  • 安装所需依赖

    1
    2
    3
    4
    5
    # loading 效果, 高版本使用的 es 模块
    npm i ora@5.4.1

    # 获取远程 git 仓库信息
    npm i axios
  • 修改 src/utils/handleGitBranches.ts 文件内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    import axios from 'axios';
    import ora from 'ora';

    const gethubApi =
    'https://api.github.com/repos/zxiaosi/lerna-project/branches';
    const giteeApi =
    'https://gitee.com/api/v5/repos/zxiaosi/lerna-project/branches';

    const spinner = ora('正在获取远程模板...');

    /** 处理接口返回数据 */
    const handleData = (origin: 'github' | 'gitee', data: any[]) => {
    spinner.stop();

    const branches = data
    ?.map((item: any) => ({ name: item.name, value: item.name }))
    ?.filter((item: any) => item.name !== 'master'); // 排除 master 分支

    return { origin, branches };
    };

    /**
    * 获取远程仓库的所有分支名
    */
    export default async function () {
    spinner.start();

    try {
    const resp = await axios.get(gethubApi);
    return handleData('github', resp.data);
    } catch (err) {
    try {
    const resp = await axios.get(giteeApi);
    return handleData('gitee', resp.data);
    } catch (err) {
    spinner.fail('获取远程模板失败!');
    throw new Error('Get remote template failed!');
    }
    }
    }

配置 simple-git 下载远程仓库

  • 安装所需依赖

    1
    2
    # 获取远程仓库
    npm i simple-git
  • 修改 src/utils/handleDownloadZip.ts 文件内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    import ora from 'ora';
    import { simpleGit, SimpleGit } from 'simple-git';

    const git: SimpleGit = simpleGit({
    progress({ method, stage, progress, processed, total }) {
    console.log(
    `git.${method} ${stage} stage ${progress}% complete, ${processed}/${total}`
    );
    },
    });

    /**
    * 下载 zip 文件
    * @param templateSource 模板源
    * @param branch 分支
    * @param outputPath 输出路径
    */
    export default function (
    templateSource: string,
    branch: string,
    outputPath: string
    ) {
    const spinner = ora(`正在从 ${templateSource} 拉取远程模板...`).start();

    return new Promise<void>(async (resolve, reject) => {
    git
    .clone(templateSource, outputPath, { '--branch': branch })
    .then((resp) => {
    spinner.succeed(`拉取远程模板成功!`);
    resolve();
    })
    .catch((err) => {
    spinner.fail(`拉取远程模板失败!`);
    reject('exit');
    });
    });
    }

配置 @inquirer/prompts 命令交互

  • 安装所需依赖

    1
    2
    # 高版本不支持 node16
    npm i @inquirer/prompts@3.3.2
  • 修改 src/utils/handleCheckFolder.ts 文件内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import fs from 'fs';

    import ora from 'ora';

    /** 检测文件夹是否为空 */
    export default function (path: string) {
    const spinner = ora('检测文件夹是否为空...').start();
    return new Promise((resolve, reject) => {
    fs.readdir(path, (err, files) => {
    if (err) {
    spinner.fail(`检测文件夹失败!`);
    return reject('exit');
    }

    if (files.length !== 0) {
    spinner.fail(`当前文件夹不为空!`);
    return reject('exit');
    }

    spinner.stop();
    return resolve(true);
    });
    });
    }
  • 修改 src/core/registerPrompts.ts 文件内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    import path from 'path';

    import { select } from '@inquirer/prompts';
    import pc from 'picocolors';

    import {
    handleCheckFolder,
    handleDownloadZip,
    handleGitBranches,
    } from '../utils';

    const modeItems = [
    { name: '本地模板', value: 'local' },
    { name: '远程模板(Github)', value: 'github' },
    { name: '远程模板(Gitee)', value: 'gitee' },
    ];

    const outputPath = path.join(
    process.cwd(),
    process.env.NODE_ENV === 'development' ? 'output' : ''
    );

    /**
    * 命令交互逻辑
    */
    export default async function () {
    const mode = await select({
    message: '请选择获取模板的方式',
    choices: modeItems,
    });

    switch (mode) {
    case 'local': {
    console.log(pc.yellow('功能正在完善中...'));
    break;
    }
    case 'github':
    case 'gitee': {
    const { origin, branches } = await handleGitBranches();

    const template = await select({
    message: '请选择要获取的模板',
    choices: branches,
    });

    try {
    const result = await handleCheckFolder(outputPath);
    if (!result) return;

    const templateSource = `https://${origin}.com/zxiaosi/lerna-project.git`;
    await handleDownloadZip(templateSource, template, outputPath);
    } catch (err: any) {
    throw new Error(err);
    }

    break;
    }
    }
    }
  • 修改 src/core/registerException.ts 文件内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /**
    * 异常捕获
    */
    export default function () {
    process.on('uncaughtException', (error) => {
    const { name, message } = error;

    if (message.includes('User force closed the prompt')) {
    console.log('\n👋 Until next time!\n');
    } else {
    if (error.message !== 'exit') {
    console.log('\n🤖 Uncaught error!\n', error.message);
    }

    process.exit(1); // 退出程序
    }
    });
    }

配置 cross-env 设置环境变量

  • 安装所需依赖

    1
    npm i cross-env -D
  • 修改 package.json 文件内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    {
    "name": "@zxiaosi/cli",
    "version": "1.0.6",
    "description": "cli tool for qiankun",
    "author": "zxiaosi",
    "license": "MIT",
    "main": "index.js",
    "types": "/dist/index.d.ts",
    "files": ["src", "dist", "templates", "index.js"],
    "repository": {
    "type": "git",
    "url": "git+https://github.com/zxiaosi/node-cli.git"
    },
    "bin": {
    "zxiaosi": "./index.js"
    },
    "keywords": ["qiankun"],
    "publishConfig": {
    "access": "public"
    },
    "dependencies": {
    "@inquirer/prompts": "^3.3.2",
    "axios": "^1.7.9",
    "chokidar": "^4.0.3",
    "figlet": "^1.8.0",
    "ora": "^5.4.1",
    "picocolors": "^1.1.1",
    "simple-git": "^3.27.0"
    },
    "devDependencies": {
    "@types/figlet": "^1.7.0",
    "@types/node": "^22.13.2",
    "cross-env": "^7.0.3",
    "typescript": "^5.7.3"
    },
    "scripts": {
    "clean": "rimraf ./node_modules",
    "dev": "tsc -w",
    "build": "rimraf ./dist && cross-env NODE_ENV=production tsc",
    "preview": "cross-env NODE_ENV=development node ./index.js",
    "preview:build": "cross-env NODE_ENV=production node ./index.js"
    }
    }

配置 src/index.ts 程序入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { registerFiglet, registerException, registerPrompts } from './core';

/** Node CLI */
export default class CLI {
appPath: string;

constructor(appPath: any) {
this.appPath = appPath || process.cwd();

registerException();
registerFiglet();
}

async run() {
await registerPrompts();
}
}

运行项目

  • 本地运行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 运行 npm run dev, 可以监听文件变化,并实时编译成 dist 目录下的文件
    # 然后新开一个命令框运行 npm run preview 查看效果。
    npm run dev
    npm run preview

    # 运行 npm run build, 编译成 dist 目录下的文件
    # 然后运行 npm run preview:build 查看效果。
    npm run build
    npm run preview:build
  • 本地模仿全局命令

    1
    2
    3
    4
    5
    6
    # 在本地全局包中生成一个软连接指向当前目录
    # 查看本地全局包 npm ls -g
    npm link

    # 执行 bin 命令
    zxiaosi
  • 上传 npm

    1
    2
    3
    4
    5
    6
    7
    8
    # 上传包
    npm publish

    # 全局安装
    npm install @zxiaosi/cli -g

    # 执行 bin 命令
    zxiaosi

npm 链接