1.0.0 • Published 1 year ago

jcae-cli v1.0.0

Weekly downloads
-
License
ISC
Repository
-
Last release
1 year ago

依赖的基础包

包名用途
commander命令行工具,读取命令行命令,知道用户想要做什么
inquirer交互式命令工具,给用户提供一个提问流方式
chalk颜色插件,用来修改命令行输出样式,通过颜色区分 info、error 日志,清晰直观
ora用于显示加载中的效果,类似于前端页面的 loading 效果,想下载模版这种耗时的操作,有了 loading 效果,可以提示用户正在进行中,请耐心等待
globby用于检索文件
fs-extranode fs 文件系统模块的增强版
pacote获取 node 包最新版本等信息
handlebars提供了必要的功能,使你可以高效地构建语义化模板
目录结构
cli-template
├─ .gitignore
├─ README.md
├─ build // 打包后文件夹
├─ project-template // 初始化项目模版
├─ bin.js // 生产环境执行文件入口,具体见下
├─ bin-local.js // 本底调试执行文件入口,具体见下
├─ package.json // 配置文件,具体见下
├─ src
│ ├─ commands // 命令文件夹
│ │ ├─ create.ts // create 命令
│ │ ├─ scope.ts // scope 命令
│ │ ├─ package.ts // package 命令
│ │ └─ utils // 公共函数
│ ├─ index.ts // 入口文件
│ └─ lib // 公共第三方包
│ ├─ consts.ts // 常量
│ ├─ index.ts
│ ├─ logger.ts // 控制台颜色输出
│ └─ spinner.ts // 控制台 loading
├─ tsconfig.json // TypeScript 配置文件
└─ tslint.json // tslint 配置文件
package.json
  • 1、npm init 初始化 package.json
  • 2、npm i typescript ts-node tslint rimraf -D 安装开发依赖
  • 3、npm i typescript chalk commander execa fs-extra globby handlebars inquirer ora pacote 安装生产依赖
  • scripts配置clear、build、publish、lint命令,具体使用最后发包会讲到

完成的package.json配置文件如下

{
  "name": "cli-template",
  "version": "0.0.2",
  "description": "cli模版仓库",
  "main": "./build",
  "scripts": {
    "clear": "rimraf build",
    "build": "npm run clear && tsc",
    "publish": "npm run build && npm publish",
    "lint": "tslint ./src/**/*.ts --fix"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/zxyue25/cli-template.git"
  },
  "bin": {
    "privatify": "./bin.js",
    "privatify-local": "./bin-local.js"
  },
  "files": ["build", "bin.js"],
  "keywords": ["cli", "node", "typescript", "command"],
  "author": "zxyue25",
  "license": "ISC",
  "devDependencies": {
    "rimraf": "^3.0.2",
    "ts-node": "^10.3.0",
    "tslint": "^6.1.3",
    "typescript": "^4.4.4"
  },
  "dependencies": {
    "chalk": "^4.1.2",
    "commander": "^8.2.0",
    "execa": "^5.1.1",
    "fs-extra": "^10.0.0",
    "globby": "^11.0.4",
    "inquirer": "^8.2.0",
    "leven": "^4.0.0",
    "ora": "^5.4.1",
    "pacote": "^12.0.2"
  }
}

重点需要关注bin字段files字段

  • bin字段见下面(2)
  • files字段即 npm 的白名单,如下图,npm 官方解释,也就是说发包后需要包括哪些文件,不配置的话默认发布全部文件,这自然是不好的,你想别人看到你的源码嘛?所以这里我们配置了"files": [ "build", "bin.js" ],包括 build 跟 bin.js,src 文件夹不在白名单内 image.png
(3) tsconfig.json 配置文件

如果一个目录下存在一个tsconfig.json文件,那么它意味着这个目录是 TypeScript 项目的根目录。 tsconfig.json文件中指定了用来编译这个项目的根文件和编译选项,按照如下配置即可

不熟悉 typescript 的可以先跳过,理解为我们利用 typescript 编写,需要配置一个配置文件即可

// tsconfig.json
{
  "compilerOptions": {
    "target": "es2017",
    "module": "commonjs",
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "alwaysStrict": true,
    "sourceMap": false,
    "noEmit": false,
    "noEmitHelpers": false,
    "importHelpers": false,
    "strictNullChecks": false,
    "allowUnreachableCode": true,
    "lib": ["es6"],
    "typeRoots": ["./node_modules/@types"],
    "outDir": "./build", // 重定向输出目录
    "rootDir": "./src" // 仅用来控制输出的目录结构
  },
  "exclude": [
    // 不参与打包的目录
    "node_modules",
    "build",
    "**/*.test.ts",
    "temp",
    "scripts",
    "./src/__tests__"
  ],
  "esModuleInterop": true,
  "allowSyntheticDefaultImports": true,
  "compileOnSave": false,
  "buildOnSave": false
}
(4)bin.js 和 npm link 本地调试
  "bin": {
    "privatify": "./bin.js",
    "privatify-local": "./bin-local.js"
  }

生产环境

// bin.js
#!/usr/bin/env node
require('./build')

本地调试

// bin-local.js
#!/usr/bin/env node
require('ts-node/register')
require('./src')
核心代码实现

src/index.ts为入口文件,src/commands/*.*s下放具体命令文件,这里我们根据3.1中设计的脚手架的命令,创建create.tsscope.tspackage.ts文件 image.png

回顾如下 image.png

可以看到,每个命令都包括command(命令)description(描述)、还有一个执行函数(action),部分还支持option(参数)

src/commands/*.*s下都写成如下形式,如create.ts

// src/commands/create.ts
const action = (projectName) => {
  console.log("projectName:", projectName);
};
export default {
  command: "create <registry-name>",
  description: "创建一个npm私服仓库",
  optionList: [["--context <context>", "上下文路径"]],
  action,
};
公共第三方依赖封装

脚手架属于交互式命令行,涉及到界面的友好提示,成功或失败的颜色、文案等,loading,图标等将此统一封装在路径src/lib下,具体不展开,比较基本的封装

初始化项目模版实现

逻辑大致与create [options] <app-name>一样,通过交互式初始化一个项目,利用inquirer包进行交互,询问用户package.jsonnamedescriptionauthor字段,拿到字段后,开始下载项目模版,项目模版存在的位置有两种实现方式,如下

  • 第一种是和脚手架打包在一起,在安装脚手架的时候就会将项目模板存放在全局目录下了,这种方式每次创建项目的时候都是从本地拷贝的速度很快,但是项目模板自身升级比较困难
  • 第二种是将项目模板存在远端仓库(比如 gitlab 仓库),这种方式每次创建项目的时候都是通过某个地址动态下载的,项目模板更新方便 image.png 我们这里先用第一种,项目模版放在project-template路径下,第二种下面讲
// project-template/package.json
{
  "name": "{{ name }}",
  "version": "1.0.0",
  "description": "{{ description }}",
  "main": "index.js",
  "author": "{{ author }}",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "license": "ISC",
  "dependencies": {
    "npm-package-privatify": "^1.1.4"
  }
}

再利用handlebars包进行模版替换,也就是把上面的{{ name }}{{ description }}{{ author }}换成用户输入的

// // src/commands/create.ts
import * as path from "path";
import * as handlebars from "handlebars";
import * as inquirer from "inquirer";
import {
  chalk,
  execa,
  fs,
  startSpinner,
  succeedSpiner,
  warn,
  info,
} from "../lib";

// 检查是否已经存在相同名字工程
export const checkProjectExist = async (targetDir) => {
  if (fs.existsSync(targetDir)) {
    const answer = await inquirer.prompt({
      type: "list",
      name: "checkExist",
      message: `\n仓库路径${targetDir}已存在,请选择`,
      choices: ["覆盖", "取消"],
    });
    if (answer.checkExist === "覆盖") {
      warn(`删除${targetDir}...`);
      fs.removeSync(targetDir);
    } else {
      return true;
    }
  }
  return false;
};

export const getQuestions = async (projectName) => {
  return await inquirer.prompt([
    {
      type: "input",
      name: "name",
      message: `package name: (${projectName})`,
      default: projectName,
    },
    {
      type: "input",
      name: "description",
      message: "description",
    },
    {
      type: "input",
      name: "author",
      message: "author",
    },
  ]);
};

export const cloneProject = async (targetDir, projectName, projectInfo) => {
  startSpinner(`开始创建私服仓库 ${chalk.cyan(targetDir)}`);
  // 复制'private-server-boilerplate'到目标路径下创建工程
  await fs.copy(
    path.join(__dirname, "..", "..", "private-server-boilerplate"),
    targetDir
  );

  // handlebars模版引擎解析用户输入的信息存在package.json
  const jsonPath = `${targetDir}/package.json`;
  const jsonContent = fs.readFileSync(jsonPath, "utf-8");
  const jsonResult = handlebars.compile(jsonContent)(projectInfo);
  fs.writeFileSync(jsonPath, jsonResult);

  // 新建工程装包
  execa.commandSync("npm install", {
    stdio: "inherit",
    cwd: targetDir,
  });

  succeedSpiner(
    `私服仓库创建完成 ${chalk.yellow(projectName)}\n👉 输入以下命令开启私服:`
  );

  info(`$ cd ${projectName}\n$ sh start.sh\n`);
};

const action = async (projectName: string, cmdArgs?: any) => {
  console.log("projectName:", projectName);
};

export default {
  command: "create <registry-name>",
  description: "创建一个npm私服仓库",
  optionList: [["--context <context>", "上下文路径"]],
  action,
};

当创建的文件存在时,提醒用户,并提供覆盖跟取消选项,执行效果如图 image.png

可以看到创建了项目name,且package.json是用户输入的值 image.png

第二种也比较简单,利用download-git-repo包,下载远程仓库地址即可,前提是你需要新建一个模版仓库,例如如下

需要注意的是,远程仓库地址,不是 git clone 的地址,而是需要稍微调整下 比如 git 仓库地址是https://github.com/zxyue25/vue-demo-cli-templateA.git -> https://github.com:zxyue25/vue-demo-cli-templateA#master

import * as download from "download-git-repo";
// https://github.com/zxyue25/vue-demo-cli-templateA.git
download(
  "https://github.com:zxyue25/vue-demo-cli-templateA#master",
  projectName,
  { clone: true },
  (err) => {
    if (err) {
      spinner.fail();
      return;
    }
    spinner.succeed();
    inquirer
      .prompt([
        {
          type: "input",
          name: "name",
          message: "请输入项目名称",
        },
        {
          type: "input",
          name: "description",
          message: "请输入项目简介",
        },
        {
          type: "input",
          name: "author",
          message: "请输入作者姓名",
        },
      ])
      .then((answers) => {
        const packagePath = `${projectName}/package.json`;
        const packageContent = fs.readFileSync(packagePath, "utf-8");
        //使用handlebars解析模板引擎
        const packageResult = handlebars.compile(packageContent)(answers);
        //将解析后的结果重写到package.json文件中
        fs.writeFileSync(packagePath, packageResult);
        console.log(logSymbols.success, chalk.yellow("初始化模板成功"));
      });
  }
);
(8)发包

到此,脚手架基本的搭建与开发就完成了,发布到 npm

  • 1、npm run lint 校验代码,毕竟都发包了,避免出现问题
  • 2、npm run build typescript 打包
  • 3、npm publish 发布到 npm 发包完成后,安装检查
npm i npm-package-privatify -g
privatify