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
- ESLint 主要解决的是代码质量问题
- Prettier 主要解决的是代码风格问题
- Prettier 会去掉你代码里的所有样式风格,然后用统一固定的格式重新输出
- prettier声称自己是一个有主见的代码格式化工具
- Prettier vs. Linters
- integrating-with-linters
- Prettier 对应的是各种 Linters 的 Formatting rules 这一类规则
- 禁用 Linters 自己的 Formatting rules,让 Prettier 接管这些职责
- eslint-config-prettier用来关闭和 Prettier 冲突非必要的规则
- recommended-configuration
- prettier-vscode
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
- commitizen插件可帮助实现一致的提交消息
- cz-customizable可以实现自定义的提交
- @commitlint/cli可以检查提交信息
- @commitlint/config-conventional检查您的常规提交
安装配置
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)
- 可以为提交类型添加一个围在圆括号内的作用域,以为其提供额外的上下文信息