lerna

lerna简介

相关文档

  • https://lerna.js.org/
  • https://lerna.nodejs.cn/
  • https://www.lernajs.cn/

Lerna是什么?

  • Lerna 是 Babel 为实现 Monorepo 开发的工具;最擅长管理依赖关系和发布
  • Lerna 优化了多包工作流,解决了多包依赖发版手动维护版本等问题
  • Lerna 不提供构建、测试等任务,工程能力较弱,项目中往往需要基于它进行顶层能力的封装

Lerna 主要做三件事

  • 为单个包或多个包运行命令 (lerna run)
  • 管理依赖项 (lerna bootstrap)
  • 发布依赖包,处理版本管理,并生成变更日志 (lerna publish)

Lerna 能解决了什么问题?

  • 代码共享,调试便捷: 一个依赖包更新,其他依赖此包的包/项目无需安装最新版本,因为 Lerna 自动 Link
  • 安装依赖,减少冗余:多个包都使用相同版本的依赖包时,Lerna 优先将依赖包安装在根目录
  • 规范版本管理: Lerna 通过 Git 检测代码变动,自动发版、更新版本号;两种模式管理多个依赖包的版本号
  • 自动生成发版日志:使用插件,根据 Git Commit 记录,自动生成 ChangeLog

Lerna 工作模式

Lerna 允许您使用两种模式来管理您的项目:固定模式(Fixed)、独立模式(Independent)

① 固定模式(Locked mode)

  • Lerna 把多个软件包当做一个整体工程,每次发布所有软件包版本号统一升级(版本一致),无论是否修改
  • 项目初始化时,lerna init 默认是 Locked mode
json复制代码{
  "version": "0.0.0"
}

② 独立模式(Independent mode)

  • Lerna 单独管理每个软件包的版本号,每次执行发布指令,Git 检查文件变动,只发版升级有调整的软件包
  • 项目初始化时,lerna init --independent
json复制代码{
  "version": "independent"
}

lerna 源码

配置安装源

npm install -g yrm
npm install -g nrm

克隆源码

git clone xxx --depth=1

调试源码

.vscode\launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "skipFiles": ["<node_internals>/**"],
      "program": "${workspaceFolder}\\core\\lerna\\cli.js",
      "args": ["ls"]
    }
  ]
}

核心包

lerna 入口核心包
@lerna/cli
@lerna/create 创建包命令
@lerna/init 初始化lerna项目

创建 npm 私服

  • verdaccio是一个简单 、零配置的本地私有化 npm 仓库
cnpm install verdaccio -g
verdaccio
http://localhost:4873
npm adduser --registry http://localhost:4873/
npm publish --registry http://localhost:4873/

创建包

lerna create lerna4 --registry http://localhost:4873
lerna success create New package lerna4 created at ./packages\lerna4
lerna create @lerna4/cli --registry http://localhost:4873
lerna success create New package @lerna4/cli created at ./packages\cli
lerna create @lerna4/create --registry http://localhost:4873
lerna success create New package @lerna4/create created at ./packages\create
lerna create @lerna4/init --registry http://localhost:4873
lerna success create New package @lerna4/init created at ./packages\init

单元测试

npm install --save-dev jest
//在所有的包下执行test命令
lerna run test
//在lerna4下执行test命令
lerna run test --scope lerna4

//在所有的包下执行shell脚本
lerna exec -- jest
//在lerna4下执行shell脚本
lerna exec --scope lerna4 -- jest

相关package.json

package.json

{
  "name": "root",
  "private": true,
  "devDependencies": {
    "lerna": "^4.0.0"
  },
+ "scripts": {
+    "test":"jest"
+  }
}

jest.config.js

module.exports = {
  testMatch: ["**/__tests__/**/*.test.js"],
};

lerna4\package.json

packages\lerna4\package.json

{
+  "scripts": {
+    "test": "jest"
+  }
}

lerna4.js

packages\lerna4\lib\lerna4.js

module.exports = lerna4;
function lerna4() {
  return "lerna4";
}

create.test.js

packages\create__tests__\create.test.js

"use strict";

const create = require("..");
describe("@lerna4/create", () => {
  it("create", () => {
    expect(create()).toEqual("create");
  });
});

eslint

  • eslint是一个插件化并且可配置的 JavaScript 语法规则和代码风格的检查工具
  • 代码质量问题:使用方式有可能有问题
  • 代码风格问题:风格不符合一定规则
  • vscode-eslint
cnpm i eslint  --save-dev

.eslintrc.js

module.exports = {
  parserOptions: { ecmaVersion: 2017, sourceType: "module" },
  extends: ["eslint:recommended"],
  rules: {
    "no-unused-vars": ["off"],
  },
  env: { node: true, jest: false },
};

.eslintignore

__tests__;

package.json

  "scripts": {
    "test": "jest",
+   "lint":"eslint --ext .js packages/**/*.js --no-error-on-unmatched-pattern --fix"
  }

Prettier

cnpm i   prettier eslint-plugin-prettier  --save-dev

.eslintrc.js

module.exports = {
  extends: ['eslint:recommended'],
  //让所有可能会与 prettier 规则存在冲突的 eslint rule失效,并使用 prettier 的规则进行代码检查
  //相当于用 prettier 的规则,覆盖掉 eslint:recommended 的部分规则
+ plugins: ['prettier'],
  rules: {
    'no-unused-vars': ['off'],
    //不符合prettier规则的代码要进行错误提示
+   'prettier/prettier': ['error', { endOfLine: 'auto' }],
  },
  env: { node: true, jest: false },
};

.prettierrc.js

module.exports = {
  singleQuote: true,
};

editorconfig

  • editorconfig帮助开发人员在不同的编辑器和 IDE 之间定义和维护一致的编码样式
  • 不同的开发人员,不同的编辑器,有不同的编码风格,而 EditorConfig 就是用来协同团队开发人员之间的代码的风格及样式规范化的一个工具,而.editorconfig 正是它的默认配置文件
  • EditorConfig
  • vscode 这类编辑器,需要自行安装 editorconfig 插件

.editorconfig

  • Unix 系统里,每行结尾只有换行,即\n LF(Line Feed)
  • Windows 系统里面,每行结尾是换行 回车,即\r\n CR/LF
  • Mac 系统里,每行结尾是回车,即\r CR(Carriage Return)
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

总结

  • 开发过程中,如果写出代码质量有问题的代码,eslint 能够及时提醒开发者,便于及时修复
  • 如果写出代码格式有问题的代码,prettier 能够自动按照我们制定的规范、格式化代码
  • 不同开发者如果使用不同的编辑器(webstorm/vscode)或系统(windows/mac),能够执行统一的代码风格标准

git hook

pre-commit

  • 可以在 git commit 之前检查代码,保证所有提交到版本库中的代码都是符合规范的
  • 可以在 git push 之前执行单元测试,保证所有的提交的代码经过的单元测试
  • husky可以让我们向项目中方便添加 git hooks
  • lint-staged 用于实现每次提交只检查本次提交所修改的文件

安装 Git hooks

cnpm i husky --save-dev
npm set-script prepare "husky install"

安装 pre-commit

npx husky add .husky/pre-commit "npx lint-staged"

commit-msg

安装配置

cnpm install commitizen cz-customizable @commitlint/cli @commitlint/config-conventional --save-dev

安装 commit-msg

npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"

添加命令

npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"

.cz-config.js

module.exports = {
  types: [
    { value: "feat", name: "feat:一个新特性" },
    { value: "fix", name: "fix:修复BUG" },
  ],
  scopes: [{ name: "sale" }, { name: "user" }, { name: "admin" }],
};

commitlint.config.js

module.exports = {
  extends: ["@commitlint/config-conventional"],
};

package.json

  "scripts": {
    "test": "jest",
    "lint": "eslint --ext .js packages/**/*.js --no-error-on-unmatched-pattern --fix",
    "prepare": "husky install",
+   "commit": "cz"
  },

发布上线

npx husky add .husky/pre-push "npm run test"
lerna version
lerna publish

安装命令

cli.js

packages\lerna4\cli.js

#!/usr/bin/env node
require(".")(process.argv.slice(2));

lerna4\index.js

packages\lerna4\index.js

module.exports = main;
function main(argv) {
  console.log(argv);
}

lerna4\package.json

packages\lerna4\package.json

{
+  "main": "index.js",
+  "bin":{
+    "lerna4":"cli.js"
+  }
}

链接

cd packages\lerna4
npm link
lerna4

yargs

  • yargs用来解析命令行参数和选项
const yargs = require("yargs/yargs");
const argv = process.argv.slice(2);
const cli = yargs(argv);
//应用到每一个命令的全局参数
const opts = {
  loglevel: {
    defaultDescription: "info",
    describe: "报告日志的级别",
    type: "string",
    alias: "L",
  },
};
//全局的key
const globalKeys = Object.keys(opts).concat(["help", "version"]);
cli
  .options(opts) //配置全局参数
  .group(globalKeys, "Global Options:") // 把全局参数分到全局组里
  .usage("Usage: $0 <command> [options]") //提示使用说明
  .demandCommand(1, "至少需要一个命令,传递--help查看所有的命令和选项") //指定最小命令数量
  .recommendCommands() //推荐命令
  .strict() //严格命令,不正确 会报错
  .fail((msg, err) => {
    //自定义错误打印
    console.error("lerna", msg, err);
  })
  .alias("h", "help") //别名
  .alias("v", "version") //别名
  .wrap(cli.terminalWidth()) //命令行宽度
  .epilogue(
    //结语
    `当1个命令失败了,所有的日志将会写入当前工作目录中的lerna-debug.log`
  )
  .command({
    command: "create <name>",
    describe: "创建一个新的lerna管理的包",
    builder: (yargs) => {
      yargs
        .positional("name", {
          describe: "包名(包含scope)",
          type: "string",
        })
        .options({
          registry: {
            group: "Command Options:",
            describe: "配置包的发布仓库",
            type: "string",
          },
        });
    },
    handler: (argv) => {
      console.log("执行init命令", argv);
    },
  })
  .parse(argv);

/**
node lerna4.js create project --registry  http://localhost:4873
执行init命令 {
  '$0': 'lerna4.js',
  _: [ 'create' ],
  name: 'project'
  registry: 'http://localhost:4873',
}
*/

跑通 init 命令

lerna link
lerna bootstrap

cli\package.json

packages\cli\package.json

"dependencies": {
    "@lerna4/cli":"^0.0.4",
    "@lerna4/init":"^0.0.4"
},
+  "main": "index.js",

cli\index.js

packages\cli\index.js

const yargs = require("yargs/yargs");
function lernaCLI() {
  const cli = yargs();
  //应用到每一个命令的全局参数
  const opts = {
    loglevel: {
      defaultDescription: "info",
      describe: "报告日志的级别",
      type: "string",
      alias: "L",
    },
  };
  //全局的key
  const globalKeys = Object.keys(opts).concat(["help", "version"]);
  return cli
    .options(opts) //配置全局参数
    .group(globalKeys, "Global Options:") // 把全局参数分到全局组里
    .usage("Usage: $0 <command> [options]") //提示使用说明
    .demandCommand(1, "至少需要一个命令,传递--help查看所有的命令和选项") //指定最小命令数量
    .recommendCommands() //推荐命令
    .strict() //严格命令,不正确 会报错
    .fail((msg, err) => {
      //自定义错误打印
      console.error("lerna", msg, err);
    })
    .alias("h", "help") //别名
    .alias("v", "version") //别名
    .wrap(cli.terminalWidth()) //命令行宽度
    .epilogue(
      //结语
      `当1个命令失败了,所有的日志将会写入当前工作目录中的lerna-debug.log`
    );
}
module.exports = lernaCLI;

init\command.js

packages\init\command.js

exports.command = "init";
exports.describe = "创建一个新的Lerna仓库";
exports.builder = (yargs) => {
  console.log("执行init builder");
};
exports.handler = (argv) => {
  console.log("执行init命令", argv);
};

lerna4\package.json

packages\lerna4\package.json

+  "main": "index.js"

lerna4\index.js

packages\lerna4\index.js

const cli = require('@lerna4/cli');
const initCmd = require('@lerna4/init/command');
function main(argv) {
  return cli().command(initCmd).parse(argv);
}

module.exports = main;

实现 init 命令

安装依赖

lerna add fs-extra  packages/init
lerna add  execa  packages/init

init\command.js

packages\init\command.js

exports.command = "init";
exports.describe = "创建一个新的Lerna仓库";
exports.builder = () => {
  console.log("执行init builder");
};
exports.handler = (argv) => {
  console.log("执行init命令", argv);
  return require(".")(argv);
};

packages\init\index.js

packages\init\index.js

const path = require("path");
const fs = require("fs-extra");
const execa = require("execa");

class InitCommand {
  constructor(argv) {
    this.argv = argv;
    this.rootPath = path.resolve();
  }
  async execute() {
    await execa("git", ["init"], { stdio: "pipe" });
    await this.ensurePackageJSON();
    await this.ensureLernaConfig();
    await this.ensurePackagesDir();
    console.log("Initialized Lerna files");
  }
  async ensurePackageJSON() {
    console.log("创建 package.json");
    await fs.writeJson(
      path.join(this.rootPath, "package.json"),
      {
        name: "root",
        private: true,
        devDependencies: {
          lerna: "^4.0.0",
        },
      },
      { spaces: 2 }
    );
  }
  async ensureLernaConfig() {
    console.log("创建 lerna.json");
    await fs.writeJson(
      path.join(this.rootPath, "lerna.json"),
      {
        packages: ["packages/*"],
        version: "0.0.0",
      },
      { spaces: 2 }
    );
  }
  async ensurePackagesDir() {
    console.log("创建 packages 目录");
    await fs.mkdirp(path.join(this.rootPath, "packages"));
  }
}
function factory(argv) {
  new InitCommand(argv).execute();
}
module.exports = factory;

实现 create 命令

安装依赖

lerna add pify  packages/crate
lerna add  init-package-json  packages/crate
lerna add  dedent  packages/crate

lerna4\index.js

packages\lerna4\index.js

const cli = require('@lerna4/cli');
const initCmd = require('@lerna4/init/command');
const createCmd = require('@lerna4/create/command');
function main(argv) {
  return cli()
   .command(initCmd)
+  .command(createCmd)
   .parse(argv);
}

module.exports = main;

lerna4\package.json

packages\lerna4\package.json

{
  "dependencies": {
    "@lerna4/cli":"^0.0.4",
    "@lerna4/init":"^0.0.4",
+   "@lerna4/create":"^0.0.4"
  },
}

create\command.js

packages\create\command.js

exports.command = "create <name>";
exports.describe = "创建一个新的lerna管理的包";
exports.builder = (yargs) => {
  console.log("执行init builder");
  yargs
    .positional("name", {
      describe: "包名(包含scope)",
      type: "string",
    })
    .options({
      registry: {
        group: "Command Options:",
        describe: "配置包的发布仓库",
        type: "string",
      },
    });
};
exports.handler = (argv) => {
  console.log("执行create命令", argv);
  return require(".")(argv);
};

create\index.js

packages\create\index.js

const path = require("path");
const fs = require("fs-extra");
const dedent = require("dedent");
const initPackageJson = require("pify")(require("init-package-json"));
class CreateCommand {
  constructor(options) {
    this.options = options;
    this.rootPath = path.resolve();
    console.log("options", options);
  }
  async execute() {
    const { name, registry } = this.options;
    this.targetDir = path.join(this.rootPath, "packages/cli");
    this.libDir = path.join(this.targetDir, "lib");
    this.testDir = path.join(this.targetDir, "__tests__");
    this.libFileName = `${name}.js`;
    this.testFileName = `${name}.test.js`;
    await fs.mkdirp(this.libDir);
    await fs.mkdirp(this.testDir);
    await this.writeLibFile();
    await this.writeTestFile();
    await this.writeReadme();
    var initFile = path.resolve(process.env.HOME, ".npm-init");
    await initPackageJson(this.targetDir, initFile);
  }
  async writeLibFile() {
    const libContent = dedent`
        module.exports = ${this.camelName};
        function ${this.camelName}() {
            // TODO
        }
    `;
    await catFile(this.libDir, this.libFileName, libContent);
  }
  async writeTestFile() {
    const testContent = dedent`
    const ${this.camelName} = require('..');
    describe('${this.pkgName}', () => {
        it('needs tests');
    });
  `;
    await catFile(this.testDir, this.testFileName, testContent);
  }
  async writeReadme() {
    const readmeContent = dedent`## Usage`;
    await catFile(this.targetDir, "README.md", readmeContent);
  }
}
function catFile(baseDir, fileName, content) {
  return fs.writeFile(path.join(baseDir, fileName), `${content}\n`);
}
function factory(argv) {
  new CreateCommand(argv).execute();
}

module.exports = factory;

参考

lerna 命令

项目初始化

命令说明
lerna init初始化项目

创建包

命令说明
lerna create创建 package
lerna add安装依赖
lerna link链接依赖

开发和测试

命令说明
lerna exec执行 shell 脚本
lerna run执行 npm 命令
lerna clean清空依赖
lerna bootstrap重新安装依赖

发布上线

命令说明
lerna version修改版本号
lerna changed查看上个版本以来的所有变更
lerna diff查看 diff
lerna publish发布项目

格式化提交

Conventional Commits

  • 规范化的git commit可以提高git log可读性,生成格式良好的changelog
  • Conventional Commits 是一种用于给提交信息增加人机可读含义的规范
  • 它提供了一组简单规则来创建清晰的提交历史
  • 通过在提交信息中描述功能、修复和破坏性变更,使这种惯例与 semver 相互对应
<类型>[可选 范围]: <描述>

[可选 正文]

[可选 脚注]

类型(type)

  • feat: 类型 为 feat 的提交表示在代码库中新增了一个功能(这和语义化版本中的 MINOR 相对应)
  • fix: 类型 为 fix 的提交表示在代码库中修复了一个 bug(这和语义化版本中的 PATCH 相对应)
  • docs: 只是更改文档
  • style: 不影响代码含义的变化(空白、格式化、缺少分号等)
  • refactor: 代码重构,既不修复错误也不添加功能
  • perf: 改进性能的代码更改
  • test: 添加确实测试或更正现有的测试
  • build: 影响构建系统或外部依赖关系的更改(示例范围:gulp、broccoli、NPM)
  • ci: 更改持续集成文件和脚本(示例范围:Travis、Circle、BrowserStack、SauceLabs)
  • chore: 其他不修改 src 或 test 文件。
  • revert: commit 回退

范围(scope)

  • 可以为提交类型添加一个围在圆括号内的作用域,以为其提供额外的上下文信息

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值