jcae-cli v1.0.0
依赖的基础包
包名 | 用途 |
---|---|
commander | 命令行工具,读取命令行命令,知道用户想要做什么 |
inquirer | 交互式命令工具,给用户提供一个提问流方式 |
chalk | 颜色插件,用来修改命令行输出样式,通过颜色区分 info、error 日志,清晰直观 |
ora | 用于显示加载中的效果,类似于前端页面的 loading 效果,想下载模版这种耗时的操作,有了 loading 效果,可以提示用户正在进行中,请耐心等待 |
globby | 用于检索文件 |
fs-extra | node 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.json2、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 文件夹不在白名单内
(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.ts
、scope.ts
、package.ts
文件
回顾如下
可以看到,每个命令都包括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.json
的name、description、author字段,拿到字段后,开始下载项目模版,项目模版存在的位置有两种实现方式,如下
- 第一种是和脚手架打包在一起,在安装脚手架的时候就会将项目模板存放在全局目录下了,这种方式每次创建项目的时候都是从本地拷贝的速度很快,但是项目模板自身升级比较困难。
- 第二种是将项目模板存在远端仓库(比如 gitlab 仓库),这种方式每次创建项目的时候都是通过某个地址动态下载的,项目模板更新方便
我们这里先用第一种,项目模版放在
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,
};
当创建的文件存在时,提醒用户,并提供覆盖跟取消选项,执行效果如图
可以看到创建了项目name
,且package.json
是用户输入的值
第二种也比较简单,利用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
1 year ago