脚手架cli开发

# 简介

只要提到脚手架你就会想到,vue-clicreate-react-appdva-cli ... 他们的特点不用多说那就是专一! 但是在公司中你会发现有以下一系列的问题!

  • 业务类型多
  • 多次造轮子,项目升级等问题
  • 公司代码规范, 无法统一, 多人协作更为方便,不需要把文件传来传去。

很多时候我们开发时需要新建项目,把已有的项目代码复制一遍,保留基础能力。(但是这个过程非常琐碎而又耗时)。那我们可以自己定制化模板,自己实现一个属于自己的脚手架。来解决这些问题

# 开发流程

# 搭建集成了三大 UI 库的脚手架工具

先幻想下要实现的功能:

根据模板初始化项目 sy-cli create project-name 初始化配置文件 sy-cli config set repo repo-name

# 必备第三方模块

# 流程总括

  • 创建脚手架执行文件
  • 使用commander.js解析命令行指令参数
  • 使用 inquirer.js 设计命令行交互
  • 创建工程化模板并push到GitHub
  • 使用 download-git-repo 下载模板
  • 使用ora 和 chalk 美化命令行
    • 使用ora增加loading效果
    • 使用chalk改变命令行颜色

# 图示流程

image-20201126092832089

# 部分相关代码

const inquirer = require('inquirer')
async function chooseTemplate(){
  const promptList = [
    {
      type: "list", // type决定交互的方式,比如当值为input的时候就是输入的形式,list就是单选,checkbox是多选...
      name: "template",
      message: "选择一个需要创建的工程化模板",
      choices: [
        {
          name: "vue (js版本的vue全家桶工程化模板)",
          value: "vue-template-default",
        },
        {
          name: "mock-server (用于模拟接口数据的本地node服务模板)",
          value: "mock-server",
        }
      ],
    },
  ];
  const answers = await inquirer.prompt(promptList);  // 执行命令行交互,并将交互的结果返回
  const {template} = answers
  console.log(`你选择的模板是:${template}`)
  return template  // 返回我们选择的模板
}

module.exports = {
  chooseTemplate
}
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
console.log(chalk.rgb(69, 39, 160)('你选择的模板是 👉'),chalk.bgRgb(69, 39, 160)(template))

// 下载前提示loading
const spinner = ora({
  text: '正在下载模板...',
  color: "yellow",
  spinner: {
    interval: 80,
    frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
  },
});
spinner.start();

const chalk = require('chalk')
const symbols = require('log-symbols')
console.log(symbols.success, chalk.green('SUCCESS'))
console.log(symbols.error, chalk.red('FAIL'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 开发实践

# 工程创建

npm init -y # 初始化package.json
npm install eslint husky --save-dev # eslint是负责代码校验工作,husky提供了git钩子功能
npx eslint --init # 初始化eslint配置文件
1
2
3

# 1.创建文件夹

├── bin
│   └── www  // 全局命令执行的根文件
├── package.json
├── src
│   ├── main.js // 入口文件
│   └── utils   // 存放工具方法
│── .huskyrc    // git hook
│── .eslintrc.json // 代码规范校验
1
2
3
4
5
6
7
8

# 2.eslint配置

配置package.json 校验src文件夹下的代码

"scripts": {
    "lint":"eslint src"
}
1
2
3

# 3.配置husky

当使用git提交前校验代码是否符合规范

{
  "hooks": {
    "pre-commit": "npm run lint"
  }
}
1
2
3
4
5

# 4.链接全局包

设置在命令下执行sy-cli时调用bin目录下的www文件

"bin": {
    "sy-cli": "./bin/www"
}
1
2
3

可以设置多入口名称

"bin": {
    "sy": "./bin/www",
    "sy-cli": "./bin/www"
}
1
2
3
4

www文件中使用main作为入口文件,并且以node环境执行此文件

#! /usr/bin/env node
require('../src/main.js');
1
2

链接包到全局下使用

npm link
1

可以成功的在命令行中使用sy-cli命令,并且可以执行main.js文件!

# 解析命令行参数

commander:The complete solution for node.js command-line interfaces

先吹一波commander,commander可以自动生成help,解析选项参数!

像这样 vue-cli --help! 像这样 vue-cli create <project-namne>

# 1.使用commander

npm install commander
1

main.js就是我们的入口文件

const program = require('commander');
program.version('0.0.1')
  .parse(process.argv); // process.argv就是用户在命令行中传入的参数
1
2
3

执行sy-cli --help 是不是已经有一提示了!

这个版本号应该使用的是当前cli项目的版本号,我们需要动态获取,并且为了方便我们将常量全部放到util下的constants文件夹中

const { name, version } = require('../../package.json');

module.exports = {
  name,
  version,
};
1
2
3
4
5
6

这样我们就可以动态获取版本号

const program = require('commander');

const { version } = require('./utils/constants');

program.version(version)
  .parse(process.argv);
1
2
3
4
5
6

# 2.配置指令命令

根据我们想要实现的功能配置执行动作,遍历产生对应的命令; 可简写设置;

const actionsMap = {
  create: { // 创建模板
    description: 'create project',
    alias: 'cr',
    examples: [
      'sy-cli create <template-name>',
    ],
  },
  config: { // 配置配置文件
    description: 'config info',
    alias: 'c',
    examples: [
      'sy-cli config get <k>',
      'sy-cli config set <k> <v>',
    ],
  },
  '*': {
    description: 'command not found',
  },
};
// 循环创建命令
Object.keys(actionsMap).forEach((action) => {
  program
    .command(action) // 命令的名称
    .alias(actionsMap[action].alias) // 命令的别名
    .description(actionsMap[action].description) // 命令的描述
    .action(() => { // 动作
      console.log(action);
    });
});

program.version(version)
  .parse(process.argv);
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

# 3.编写help命令

监听help命令打印帮助信息

program.on('--help', () => {
  console.log('Examples');
  Object.keys(actionsMap).forEach((action) => {
    (actionsMap[action].examples || []).forEach((example) => {
      console.log(`  ${example}`);
    });
  });
});
1
2
3
4
5
6
7
8

# 4.找到输出的参数【要点】

// {force:true}
const cleanArgs = (cmd) => {
  const args = {};
  cmd.options.forEach((o) => {
    const key = o.long.slice(2);
    if (cmd[key]) args[key] = cmd[key];
  });
  return args;
};

program
  .command("config [value]")
  .alias('cf')
  .description("inspect and modify the config")
  .option("-g, --get <path>", "get value from option")
  .option("-s, --set <path> <value>")
  .option("-d, --delete <path>", "delete option from config")
  .action((value, cmd) => {
    console.log(value, cleanArgs(cmd)); // 调用config模块去实现
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# create命令

create命令的主要作用就是去git仓库中拉取模板并下载对应的版本到本地,如果有模板则根据用户填写的信息渲染好模板,生成到当前运行命令的目录下~

action(() => { // 动作
  if (action === '*') { // 如果动作没匹配到说明输入有误
    console.log(acitonMap[action].description);
  } else { // 引用对应的动作文件 将参数传入
    require(path.resolve(__dirname, action))(...process.argv.slice(3));
  }
}
1
2
3
4
5
6
7

根据不同的动作,动态引入对应模块的文件

创建create.js

// 创建项目
module.exports = async (projectName) => {
  console.log(projectName);
};
1
2
3
4

执行sy-cli create project,可以打印出 project

# 1.拉取项目

我们需要获取仓库中的所有模板信息,我的模板全部放在了git上,这里就以git为例,我通过axios去获取相关的信息~~~

npm i axios
1

这里借助下github的 api (opens new window)

const axios = require('axios');
// 1).获取仓库列表
const fetchRepoList = async () => {
  // 获取当前组织中的所有仓库信息,这个仓库中存放的都是项目模板
  const { data } = await axios.get('https://api.github.com/orgs/sy-cli/repos');
  return data;
};

module.exports = async (projectName) => {
  let repos = await fetchRepoList();
  repos = repos.map((item) => item.name);
  console.log(repos)
};
1
2
3
4
5
6
7
8
9
10
11
12
13

发现在安装的时候体验很不好没有任何提示,而且最终的结果我希望是可以供用户选择的!

# 2.inquirer & ora

来解决上面提到的问题

npm i inquirer ora 
1
module.exports = async (projectName) => {
  const spinner = ora('fetching repo list');
  spinner.start(); // 开始loading
  let repos = await fetchRepoList();
  spinner.succeed(); // 结束loading
  //proce.fail()// 下载失败调用
  //proce.succeed()// 下载成功调用

  // 选择模板
  repos = repos.map((item) => item.name);
  const { repo } = await Inquirer.prompt({
    name: 'repo',
    type: 'list',
    message: 'please choice repo template to create project',
    choices: repos, // 选择模式
  });
  console.log(repo);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

看到的命令行中选择的功能基本都是基于inquirer实现的,可以实现不同的询问方式;

其他提示的优化

if (err) {
    lqProcess.fail()
    console.log(symbols.error, chalk.red(err))
} else {
    lqProcess.succeed()
    console.log(symbols.success, chalk.green('创建成功'))
}
1
2
3
4
5
6
7

# 3.获取版本信息

和获取模板一样,我们可以故技重施

const fetchTagList = async (repo) => {
  const { data } = await axios.get(`https://api.github.com/repos/sy-cli/${repo}/tags`);
  return data;
};
// 获取版本信息
spinner = ora('fetching repo tags');
spinner.start();
let tags = await fetchTagList(repo);
spinner.succeed(); // 结束loading

// 选择版本
tags = tags.map((item) => item.name);
const { tag } = await Inquirer.prompt({
  name: 'tag',
  type: 'list',
  message: 'please choice repo template to create project',
  choices: tags,
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

发现每次都需要去开启loading、关闭loading,重复的代码!我们来简单的封装下;

封装请求库处理方法:

const wrapFetchAddLoding = (fn, message) => async (...args) => {
  const spinner = ora(message);
  spinner.start(); // 开始loading
  const r = await fn(...args);
  spinner.succeed(); // 结束loading
  return r;
};
// 这回用起来舒心多了~~~
let repos = await wrapFetchAddLoding(fetchRepoList, 'fetching repo list')();
let tags = await wrapFetchAddLoding(fetchTagList, 'fetching tag list')(repo);
1
2
3
4
5
6
7
8
9
10

# 4.下载项目

我们已经成功获取到了项目模板名称和对应的版本,那我们就可以直接下载啦!

npm i download-git-repo #其实也可以直接用git clone实现下载功能
1

很遗憾的是这个方法不是promise方法,没关系我们自己包装一下

const { promisify } = require('util');
const downLoadGit = require('download-git-repo');
downLoadGit = promisify(downLoadGit);
1
2
3

node中已经帮你提供了方法,将异步的api可以快速转化成promise的形式~

下载前先找个临时目录来存放下载的文件,来~继续配置常量;下载目录处理;

const downloadDirectory = `${process.env[process.platform === 'darwin' ? 'HOME' : 'USERPROFILE']}/.template`;
1

这里我们将文件下载到当前用户下的.template文件中,由于系统的不同目录获取方式不一样,process.platform 在windows下获取的是 win32 我这里是mac 所有获取的值是 darwin,在根据对应的环境变量获取到用户目录

const download = async (repo, tag) => {
  let api = `sy-cli/${repo}`; // 下载项目
  if (tag) {
    api += `#${tag}`;
  }
  const dest = `${downloadDirectory}/${repo}`; // 将模板下载到对应的目录中
  await downLoadGit(api, dest);
  return dest; // 返回下载目录
};

// 下载项目
const target = await wrapFetchAddLoding(download, 'download template')(repo, tag);
1
2
3
4
5
6
7
8
9
10
11
12

ncp拷贝/可直接fs-extra

对于简单的项目可以直接把下载好的项目拷贝到当前执行命令的目录下即可。

安装ncp可以实现文件的拷贝功能

npm i ncp
1

像这样:

let ncp = require('ncp'); 
ncp = promisify(ncp);
// 将下载的文件拷贝到当前执行命令的目录下
await ncp(target, path.join(path.resolve(), projectName));
1
2
3
4

当然这里可以做的更严谨一些,判断一下当前目录下是否有重名文件等..., 还有很多细节也需要考虑像多次创建项目是否要利用已经下载好的模板,大家可以自由的发挥~

# 5.模板编译

刚才说的是简单文件,那当然直接拷贝就好了,但是有的时候用户可以定制下载模板中的内容,拿package.json文件为例,用户可以根据提示给项目命名、设置描述等

在项目模板中增加ask.js

module.exports = [
    {
      type: 'confirm',
      name: 'private',
      message: 'ths resgistery is private?',
    },
    ...
]
1
2
3
4
5
6
7
8

根据对应的询问生成最终的package.json

下载的模板中使用了ejs模板

{
  "name": "vue-template",
  "version": "0.1.2",
  "private": "<%=private%>",
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build"
  },
  "dependencies": {
    "vue": "^2.6.10"
  },
  "autor":"<%=author%>",
  "description": "<%=description%>",
  "devDependencies": {
    "@vue/cli-service": "^3.11.0",
    "vue-template-compiler": "^2.6.10"
  },
  "license": "<%=license%>"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

写到这里,大家应该想到了!核心原理就是将下载的模板文件,依次遍历根据用户填写的信息渲染模板,将渲染好的结果拷贝到执行命令的目录下

安装需要用到的模块【要点】

npm i metalsmith ejs consolidate
1
const MetalSmith = require('metalsmith'); // 遍历文件夹
let { render } = require('consolidate').ejs;
render = promisify(render); // 包装渲染方法

// 没有ask文件说明不需要编译
if (!fs.existsSync(path.join(target, 'ask.js'))) {
  await ncp(target, path.join(path.resolve(), projectName));
} else {
  await new Promise((resovle, reject) => {
    MetalSmith(__dirname)
      .source(target) // 遍历下载的目录
      .destination(path.join(path.resolve(), projectName)) // 输出渲染后的结果
      .use(async (files, metal, done) => {
        // 弹框询问用户
        const result = await Inquirer.prompt(require(path.join(target, 'ask.js')));
        const data = metal.metadata();
        Object.assign(data, result); // 将询问的结果放到metadata中保证在下一个中间件中可以获取到
        delete files['ask.js'];
        done();
      })
      .use((files, metal, done) => {
        Reflect.ownKeys(files).forEach(async (file) => {
          let content = files[file].contents.toString(); // 获取文件中的内容
          if (file.includes('.js') || file.includes('.json')) { // 如果是js或者json才有可能是模板
            if (content.includes('<%')) { // 文件中用<% 我才需要编译
              content = await render(content, metal.metadata()); // 用数据渲染模板
              files[file].contents = Buffer.from(content); // 渲染好的结果替换即可
            }
          }
        });
        done();
      })
      .build((err) => { // 执行中间件
        if (!err) {
          resovle();
        } else {
          reject();
        }
      });
  });
}
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

这里的逻辑就是上面描述的那样,实现了模板替换!到此安装项目的功能就完成了,我们发现这里面所有用到的地址的路径都写死了,我们希望这是一个更通用的脚手架,可以让用户自己配置拉取的地址~

# config命令

新建config.js 主要的作用其实就是配置文件的读写操作,当然如果配置文件不存在需要提供默认的值,先来编写常量

constants.js的配置

const configFile = `${process.env[process.platform === 'darwin' ? 'HOME' : 'USERPROFILE']}/.zhurc`; // 配置文件的存储位置
const defaultConfig = {
  repo: 'sy-cli', // 默认拉取的仓库名
};
1
2
3
4

编写config.js

const fs = require('fs');
const { defaultConfig, configFile } = require('./util/constants');
module.exports = (action, k, v) => {
  if (action === 'get') {
    console.log('获取');
  } else if (action === 'set') {
    console.log('设置');
  }
  // ...
};
1
2
3
4
5
6
7
8
9
10

一般rc类型的配置文件都是ini格式也就是:

repo=sy-cli
register=github
1
2

下载 ini 模块解析配置文件

npm i ini
1

这里的代码很简单,无非就是文件操作了

const fs = require('fs');
const { encode, decode } = require('ini');
const { defaultConfig, configFile } = require('./util/constants');

const fs = require('fs');
const { encode, decode } = require('ini');
const { defaultConfig, configFile } = require('./util/constants');

module.exports = (action, k, v) => {
  const flag = fs.existsSync(configFile);
  const obj = {};
  if (flag) { // 配置文件存在
    const content = fs.readFileSync(configFile, 'utf8');
    const c = decode(content); // 将文件解析成对象
    Object.assign(obj, c);
  }
  if (action === 'get') {
    console.log(obj[k] || defaultConfig[k]);
  } else if (action === 'set') {
    obj[k] = v;
    fs.writeFileSync(configFile, encode(obj)); // 将内容转化ini格式写入到字符串中
    console.log(`${k}=${v}`);
  } else if (action === 'getVal') { 
    return obj[k];
  }
};
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

getVal这个方法是为了在执行create命令时可以获取到配置变量

const config = require('./config');
const repoUrl = config('getVal', 'repo');
1
2

这样我们可以将create方法中所有的sy-cli全部用获取到的值替换掉啦!

到此基本核心的方法已经ok!剩下的大家可以自行扩展啦!

# 关于exts

远程仓库增加exts接口目录,其中放置 ask.js, hook.js, 方便扩展,若没有则使用内置的ask

ask.js

可以是数组, 可以是一个返回数组的方法;遵循 Inquirer prompt参数 (opens new window)

hook.js

目前只提供生成成功后方法也就是after,提供给开发者作扩展用。after函数,可以是async function, 返回Promise实例的函数,普通函数。

ask.js

/*
 * @Author: samy
 * @email: yessz#foxmail.com
 * @time: 2020-07-05 19:41:12
 * @modAuthor: samy
 * @modTime: 2020-12-22 00:34:19
 * @desc: ask替换文件 ejs文件替换
 * @Copyright © 2015~2020 BDP FE
 */
const { execSync } = require("child_process")

module.exports = function askCreator(template = "") {
  let user = execSync("git config --global user.name", { encoding: "utf-8" })
  let email = execSync("git config --global user.email", { encoding: "utf-8" })
  user = user.trim()
  email = email.trim()
  return [
    {
      type: "input",
      name: "name",
      message: "package name(en)",
      default: template,
      validate(input) {
        const done = this.async()
        if (input.trim().length === 0) {
          done("project name is empty")
          return
        }
        done(null, true)
      },
    },
    {
      type: "confirm",
      name: "private",
      message: "Is the project private ?",
    },
    {
      type: "input",
      name: "description",
      message: "description",
    },
    {
      type: "input",
      name: "author",
      message: "author",
      default: email,
    },
    {
      type: "list",
      name: "license",
      message: "license",
      choices: [
        "MIT",
        "BSD 2-clause 'Simplified'",
        "Apache 2.0",
        "GNU General Public v3.0",
        "BSD 3-clause",
        "Eclipse Public 1.0",
        "GNU Affero General Public v3.0",
        "GNU General Public v2.0",
        "GNU Lesser General Public v2.1",
        "GNU Lesser General Public v3.0",
        "Mozilla Public 2.0",
        "The Unlicense",
      ],
    },
    {
      type: "input",
      name: "proName",
      message: "sub sys pro name (en)",
      validate(input) {
        const done = this.async()
        if (input.trim().length === 0) {
          done("sub pro name is empty")
          return
        }
        done(null, true)
      },
    },
    // {
    //   type: "input",
    //   name: "git",
    //   message: "user/repo",
    //   default: `${user}/${template}`,
    //   validate(input) {
    //     const done = this.async();
    //     if (!/\w+\/\w+/.test(input)) {
    //       done("Please input like user/repo");
    //       return;
    //     }
    //     done(null, true);
    //   },
    // },
  ]
}

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

askblk.js

/*
 * @Author: samy
 * @email: yessz#foxmail.com
 * @time: 2020-07-05 19:41:12
 * @modAuthor: samy
 * @modTime: 2020-12-22 00:37:37
 * @desc: blk 模式下 ask替换文件 ejs文件替换
 * @Copyright © 2015~2020 BDP FE
 */
module.exports = function askCreator(isMod, template = "") {
  const header = [
    {
      type: "input",
      name: "proName",
      message: "current pro name(en)",
      validate(input) {
        const done = this.async();
        if (input.trim().length === 0) {
          done("pro name is empty");
          return;
        }
        done(null, true);
      },
    },
  ];
  const mod = [
    {
      type: "input",
      name: "moduleName",
      message: "当前模块名(en)",
      default: isMod ? template : "",
      validate(input) {
        const done = this.async();
        if (input.trim().length === 0) {
          done("module name is empty");
          return;
        }
        done(null, true);
      },
    },
  ];
  const page = [
    {
      type: "input",
      name: "pageName",
      message: "pageName(en)",
      default: !isMod ? template : "",
      validate(input) {
        const done = this.async();
        if (input.trim().length === 0) {
          done("page name name path is empty");
          return;
        }
        done(null, true);
      },
    },
  ];
  const msg = isMod ? "Module" : "Page";
  const footer = [
    {
      type: "input",
      name: "name",
      message: `${msg} name title(cn)`,
      default: `${msg} case`,
    },
  ];
  if (isMod) {
    return [...header, ...mod, ...footer];
  } else {js
    return [...header, ...mod, ...page, ...footer];
  }
};

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73

# 简单示例

#!/usr/bin/env node
const chalk = require('chalk')
console.log('Hello, cli!')
console.log(chalk.green('init创建'))
const fs = require('fs')
const program = require('commander')
const download = require('download-git-repo')
const inquirer = require('inquirer')
const ora = require('ora')
const symbols = require('log-symbols')
const handlebars = require('handlebars')
program
  .version(require('./package').version, '-v, --version')
  .command('init <name>')
  .action(name => {
    console.log(name)
    inquirer
      .prompt([
        {
          type: 'input',
          name: 'author',
          message: '请输入你的名字'
        }
      ])
      .then(answers => {
        console.log(answers.author)
        const lqProcess = ora('正在创建...')
        lqProcess.start()
        download(
          'direct:https://github.com/bdp-tpl/xxx.git',
          name,
          { clone: true },
          err => {
            if (err) {
              lqProcess.fail()
              console.log(symbols.error, chalk.red(err))
            } else {
              lqProcess.succeed()
              const fileName = `${name}/package.json`
              const meta = {
                name,
                author: answers.author
              }
              if (fs.existsSync(fileName)) {
                const content = fs.readFileSync(fileName).toString()
                const result = handlebars.compile(content)(meta)
                fs.writeFileSync(fileName, result)
              }
              console.log(symbols.success, chalk.green('创建成功'))
            }
          }
        )
      })
  })
program.parse(process.argv)
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

# 项目发布

# npm

先确保是npm源,再登陆发布处理;

nrm use npm
npm publish # 已经发布成功~~
1
2

推荐发布到github中,方便和项目统一管理;

  "scripts": {
    "c": "babel src -d lib",
    "w": "npm run c -- -watch",
    "start": "npm run w",
    "b": "npm run c && npm publish",
    "bc": "npm --force unpublish rat-cli"
  },
1
2
3
4
5
6
7

可以通过npm install sy-cli -g 进行安装啦!

# github

# 1.修改 package.json

  • name为 @用户名/原包名
  • 把仓库上传到github,并把地址贴到repository里,仓库的名字和包名是一样的
  • registry为 https://npm.pkg.github.com/用户名
"name": "@samyzh/rat-cli",
"repository":"https://github.com/samyzh/rat-cli",
"publishConfig": {
    "registry": "https://npm.pkg.github.com/samyzh"
},
1
2
3
4
5

会让你输入密码,这个密码是githubtoken,注意,是githubtoken ,是githubtoken。重要的事说3遍,因为这个设计有点反人类,它是粘贴并且看不见粘贴上没有。githubtoken在github的setting里面生成。

# 2.登录npm

npm login --registry=https://npm.pkg.github.com --scope=@samyzh
username 为你的github用户名
password 为刚才第一步生成的token# PKG_TOKEN
1
2
3

# 3. 发包

npm run build
npm publish
1
2

# 4.在github上搜索包名

# 项目版本号管理

package.json中的version字段代表的是该项目的版本号。每当项目发布新版本时,需要将version字段进行相应的更新以便后期维护。虽然可以手动的修改vsersion字段,但是为了整个发布过程的自动化,尽量使用 npm version (opens new window) 指令来自动更新version

npm version (v)1.2.3  # 显示设置版本号为 1.2.3 
npm version major  # 大版本号加 1,其余版本号归 0
npm version minor  # 小版本号加 1,修订号归 0
npm version patch  # 修订号加 1
1
2
3
4

显示的设置版本号时,版本号必须符合semver规范,允许在版本号前加上个v标识。

如果不想让此次更新正式发布,还可以创建预发布版本:

# 当前版本号为 1.2.3
npm version prepatch# 版本号变为 1.2.4-0,也就是 1.2.4 版本的第一个预发布版本
npm version preminor# 版本号变为 1.3.0-0,也就是 1.3.0 版本的第一个预发布版本
npm version premajor# 版本号变为 2.0.0-0,也就是 2.0.0 版本的第一个预发布版本
npm version prerelease# 版本号变为 2.0.0-1,也就是使预发布版本号加一
1
2
3
4
5

git环境中,执行npm version修改完版本号以后,还会默认执行git add->git commit->git tag操作

img

其中commit message默认是自动修改完的版本号,可以通过添加-m/--message选项来自定义commit message

npm version xxx -m "upgrade to %s for reasons"  # %s 会自动替换为新版本号
1

比如执行npm version minor -m "feat(version): upgrade to %s for reasons"后:

img

如果git工作区还有未提交的修改,npm version将会执行失败,可以加上-f/--force后缀来强制执行。

如果不想让npm version指令影响你的git仓库,可以在指令中使用`--no-git-tag-v

# 相关问题

# 渲染特殊文件

# 解析碰到的问题;Or, if you meant to create an async function, pass async: true as an option.

修复render.js if (content.includes(['<%=', '<%-'])) {

const extList = [
  ".js",
  ".json",
  ".jsx",
  ".ts",
  ".tsx",
  ".css",
  ".less",
  ".sass",
];
export default function () {
  return function _render(files, metal, done) {
    const meta = metal.metadata();
    Object.keys(files).forEach(async (file) => {
      const ext = path.extname(file);
      if (extList.includes(ext)) {
        let content = files[file].contents.toString();
        if (content.includes(['<%=', '<%-'])) {
          content = await render(content, meta);
          files[file].contents = Buffer.from(content);
        }
      }
    });
    done();
  };
}
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

download-git-repo 下载出错 status 128

  return new Promise((resolve, reject) => {
        const spinner = ora(`正在下载模板`)
        spinner.start();
        console.log(downLoadURL)
        download(downLoadURL, target, {
            clone: true
        }, (err) => {
            if (err) {
                let errStr = err.toString()
                spinner.fail();
                reject(err);

                if (errStr.includes("status 128")) {
                    console.log('\n', logSymbols.warning, chalk.yellow("Git默认开启了SSL验证,执行下面命令关闭后再重试即可;"))
                    console.log(logSymbols.info, chalk.green("git config --global http.sslVerify false"))
                }
            } else {
                spinner.succeed();
                resolve(target);
            }
        })
    })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

最后还是有data-set.min.js做相关的设置,目前在document.ejs中先移除,不做extend处理;直接用npm引入;

发现还是不行,得还原代码;做.min.js不做替换处理;通过ignore处理; 排除处理;

 metalsmith
      .metadata(answers)
      .source("./")
      //.ignore('public')

 const ext = path.extname(file);
      const basename = path.basename(file)
      const lastSplits = basename.split('.').slice(-2)
      const hasIncludeMin = lastSplits.includes('min') // 移除.min.js包好
      if (extList.includes(ext) && !hasIncludeMin) {
1
2
3
4
5
6
7
8
9
10

# 处理md头部

<% content="---" %><%= content %>
pageComponent: 
  name: Catalogue
  data: 
    key: 01.体系1
    imgUrl: /img/web.png
    description: 体系1相关文档
title: 体系1
date: 2020-03-11 21:50:55
permalink: /sys1/
sidebar: false
article: false
comment: false
editLink: false
<%= content %>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 内置相关字段

内置 author, email, day, time,用于处理特殊替换

function getDefaultEjs() {
  let { author, email } = getGItInfo();
  let day = getNowFormatDay();
  let time = getNowFormatTime();
  return { author, email, day, time };
}

  let loader, hook, reply;
  const defaultEjs = getDefaultEjs();
  reply = await inquirer.prompt(ask);
  loader = loading("👉生成中...", dir);
  const ejsInfo = Object.assign(defaultEjs, reply);
  await metal(resolve(proPath), resolve(root, dir), ejsInfo);
  await remove(`${resolve(dir, exts.dir)}`);
  loader.succeed(`👏生成完成 ${dir}`);
  try {
    hook = betterRequire(`${proPath}/${exts.hook}`);
  } catch (e) {
    hook = { after() {} };
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 相关链接

https://docs.gitlab.com/ee/api/

https://docs.github.com/en/free-pro-team@latest/rest

https://samyzh.github.io/rat-cli/

上次更新: 2022/04/15, 05:41:30
×