源码阅读准备:
- 下载源码
- 安装依赖
- IDE 打开(webstorm)
源码阅读准备完成的标准
- 找到入口文件
- package.json bin 配置
"bin": {
"lerna": "core/lerna/cli.js"
}
- 能够本地调试
- 使用vscode调试方法
- 根目录新建
.vscode目录 - 新建.vscode/launch.json文件
{ "configurations": [ { "type": "node", "request": "launch", "name": "nodemon", "runtimeExecutable": "nodemon", "program": "${workspaceFolder}/core/lerna/cli.js", "restart": true, "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", //参数是名称和值一组,多个参数,数组里添加即可,调试时会自动附加上去 "args": ["list"] } ] } - 根目录新建
- 使用webstorm调试
- 使用vscode调试方法
解析入口文件
#!/usr/bin/env node
"use strict";
/* eslint-disable impo
rt/no-dynamic-require, global-require */
const importLocal = require("import-local");
if (importLocal(__filename)) {
require("npmlog").info("cli", "using local version of lerna");
} else {
require(".")(process.argv.slice(2)); //执行该步骤
}
- require('.') 指的相对路径,当前目录下的index.js,相当于require('./index.js')
- 查看index.js
"use strict";
const cli = require("@lerna/cli"); //指向了core/cli/index.js 几乎就是使用了yargs脚手架的命令,按理说应该是node_modules下面的@lerna/cli,但是core依赖对应package.json文件的依赖名称就就是lerna,所以直接找了core/cli/index.js
const addCmd = require("@lerna/add/command");
const bootstrapCmd = require("@lerna/bootstrap/command");
const changedCmd = require("@lerna/changed/command");
const cleanCmd = require("@lerna/clean/command");
const createCmd = require("@lerna/create/command");
const diffCmd = require("@lerna/diff/command");
const execCmd = require("@lerna/exec/command");
const importCmd = require("@lerna/import/command");
const infoCmd = require("@lerna/info/command");
const initCmd = require("@lerna/init/command");
const linkCmd = require("@lerna/link/command");
const listCmd = require("@lerna/list/command");
const publishCmd = require("@lerna/publish/command");
const runCmd = require("@lerna/run/command");
const versionCmd = require("@lerna/version/command");
const pkg = require("./package.json");
module.exports = main;
function main(argv) {
const context = { //注入版本号
lernaVersion: pkg.version,
};
return cli()
//全部都是在注册yargs命令
.command(addCmd)
.command(bootstrapCmd)
.command(changedCmd)
.command(cleanCmd)
.command(createCmd)
.command(diffCmd)
.command(execCmd)
.command(importCmd)
.command(infoCmd)
.command(initCmd)
.command(linkCmd)
.command(listCmd)
.command(publishCmd)
.command(runCmd)
.command(versionCmd)
.parse(argv, context ); //注入参数和版本号
}
- 查看
const cli = require("@lerna/cli");这个引用做了什么- 返回了globalOptions(cli)方法,应该就是注册了globalOptions的yargs
- 查看globalOptions(cli)
"use strict";
const dedent = require("dedent"); //处理缩进的库
const log = require("npmlog");
const yargs = require("yargs/yargs"); //使用了yargs脚手架
const globalOptions = require("@lerna/global-options"); //进行globalOptions注册
module.exports = lernaCLI;
function lernaCLI(argv, cwd) {
const cli = yargs(argv, cwd);
return globalOptions(cli)
.usage("Usage: $0 <command> [options]")
.demandCommand(1, "A command is required. Pass --help to see all available commands and options.")
.recommendCommands()
.strict()
.fail((msg, err) => {
// certain yargs validations throw strings :P
const actual = err || new Error(msg);
// ValidationErrors are already logged, as are package errors
if (actual.name !== "ValidationError" && !actual.pkg) {
// the recommendCommands() message is too terse
if (/Did you mean/.test(actual.message)) {
log.error("lerna", `Unknown command "${cli.parsed.argv._[0]}"`);
}
log.error("lerna", actual.message);
}
// exit non-zero so the CLI can be usefully chained
cli.exit(actual.code > 0 ? actual.code : 1, actual);
})
.alias("h", "help")
.alias("v", "version")
.wrap(cli.terminalWidth()).epilogue(dedent`
When a command fails, all logs are written to lerna-debug.log in the current working directory.
For more information, find our manual at https://github.com/lerna/lerna
`);
}
- 查看globaOptions
- 接收yargs参数,注册完成globalOptions之后又将yargs返回
"use strict";
const os = require("os");
module.exports = globalOptions;
function globalOptions(yargs) {
// the global options applicable to _every_ command
const opts = {
loglevel: {
defaultDescription: "info",
describe: "What level of logs to report.",
type: "string",
},
concurrency: {
defaultDescription: os.cpus().length,
describe: "How many processes to use when lerna parallelizes tasks.",
type: "number",
requiresArg: true,
},
"reject-cycles": {
describe: "Fail if a cycle is detected among dependencies.",
type: "boolean",
},
"no-progress": {
describe: "Disable progress bars. (Always off in CI)",
type: "boolean",
},
progress: {
// proxy for --no-progress
hidden: true,
type: "boolean",
},
"no-sort": {
describe: "Do not sort packages topologically (dependencies before dependents).",
type: "boolean",
},
sort: {
// proxy for --no-sort
hidden: true,
type: "boolean",
},
"max-buffer": {
describe: "Set max-buffer (in bytes) for subcommand execution",
type: "number",
requiresArg: true,
},
};
// group options under "Global Options:" header
const globalKeys = Object.keys(opts).concat(["help", "version"]);
return yargs
.options(opts)
.group(globalKeys, "Global Options:")
.option("ci", {
hidden: true, //隐藏option
type: "boolean",
});
}
入口文件初始化解析总结
-
使用yargs进行命令和参数的注册
-
注册globalOptions
module.exports = globalOptions; function globalOptions(yargs) { // the global options applicable to _every_ command const opts = { loglevel: { defaultDescription: "info", describe: "What level of logs to report.", type: "string", }, concurrency: { defaultDescription: os.cpus().length, describe: "How many processes to use when lerna parallelizes tasks.", type: "number", requiresArg: true, }, "reject-cycles": { describe: "Fail if a cycle is detected among dependencies.", type: "boolean", }, "no-progress": { describe: "Disable progress bars. (Always off in CI)", type: "boolean", }, progress: { // proxy for --no-progress hidden: true, type: "boolean", }, "no-sort": { describe: "Do not sort packages topologically (dependencies before dependents).", type: "boolean", }, sort: { // proxy for --no-sort hidden: true, type: "boolean", }, "max-buffer": { describe: "Set max-buffer (in bytes) for subcommand execution", type: "number", requiresArg: true, }, }; // group options under "Global Options:" header const globalKeys = Object.keys(opts).concat(["help", "version"]); return yargs .options(opts) .group(globalKeys, "Global Options:") .option("ci", { hidden: true, type: "boolean", }); } -
定义
- 定义使用方法:
.usage("Usage: $0 <command> [options]")
- 定义至少需要接收一个参数:
.demandCommand(1, "A command is required. Pass --help to see all available commands and options.")
-
定义如果命令或者参数输入错误提示最相近的命令
recommendCommands() -
定义如果输入的命令没有,则提示未识别的参数:.strict()
-
定义了如果命令输入错误,错误提示是什么
.fail((msg, err) => { // certain yargs validations throw strings :P const actual = err || new Error(msg); // ValidationErrors are already logged, as are package errors if (actual.name !== "ValidationError" && !actual.pkg) { // the recommendCommands() message is too terse if (/Did you mean/.test(actual.message)) { log.error("lerna", `Unknown command "${cli.parsed.argv._[0]}"`); } log.error("lerna", actual.message); } // exit non-zero so the CLI can be usefully chained cli.exit(actual.code > 0 ? actual.code : 1, actual); })rainbow@MacBook-Pro-673 lerna % lerna D ERR! lerna 是指 ls? -
定义参数别名
可以使用
lerna -hlerna --help.alias("h", "help") .alias("v", "version")
-
定义了命令内容的宽度为终端的整个宽度
.wrap(cli.terminalWidth()) -
定义了命令输入内容结尾显示的内容
.epilogue(dedent` When a command fails, all logs are written to lerna-debug.log in the current working directory. For more information, find our manual at https://github.com/lerna/lerna `);
-
定义了各种命令
.command(addCmd) .command(bootstrapCmd) .command(changedCmd) .command(cleanCmd) .command(createCmd) .command(diffCmd) .command(execCmd) .command(importCmd) .command(infoCmd) .command(initCmd) .command(linkCmd) .command(listCmd) .command(publishCmd) .command(runCmd) .command(versionCmd) .parse(argv, context);
查看Lerna ls命令干了什么
查看lerna list实际上执行的是const listCmd = require("@lerna/list/command");
查看这个引用
"use strict";
/**
* @see https://github.com/yargs/yargs/blob/master/docs/advanced.md#providing-a-command-module
*/
exports.command = "link";
exports.describe = "Symlink together all packages that are dependencies of each other";
//builer:在执行命令之前注册参数
exports.builder = yargs => {
yargs.options({
"force-local": {
group: "Command Options:",
describe: "Force local sibling links regardless of version range match",
type: "boolean",
},
contents: {
group: "Command Options:",
describe: "Subdirectory to use as the source of the symlink. Must apply to ALL packages.",
type: "string",
defaultDescription: ".",
},
});
//真正执行命令
return yargs.command(
"convert",
"Replace local sibling version ranges with relative file: specifiers",
() => {},
handler
);
};
//执行真正的命令
exports.handler = handler;
function handler(argv) {
return require(".")(argv);
}
查看这块代码,也就是当前目录的index.js
function handler(argv) {
return require(".")(argv);
}
"use strict";
const Command = require("@lerna/command");
const listable = require("@lerna/listable");
const output = require("@lerna/output");
const { getFilteredPackages } = require("@lerna/filter-options");
module.exports = factory;
function factory(argv) {
return new ListCommand(argv);
}
class ListCommand extends Command {
get requiresGit() {
return false;
}
initialize() {
let chain = Promise.resolve();
chain = chain.then(() => getFilteredPackages(this.packageGraph, this.execOpts, this.options));
chain = chain.then(filteredPackages => {
this.result = listable.format(filteredPackages, this.options);
});
return chain;
}
execute() {
// piping to `wc -l` should not yield 1 when no packages matched
if (this.result.text.length) {
output(this.result.text);
}
this.logger.success(
"found",
"%d %s",
this.result.count,
this.result.count === 1 ? "package" : "packages"
);
}
}
module.exports.ListCommand = ListCommand;
查看继承的父类Command,父类中定义了constructor,执行constructor方法
core/command/index.js
class Command {
constructor(_argv) {
log.pause();
log.heading = "lerna";
const argv = cloneDeep(_argv); //深拷贝参数
log.silly("argv", argv);
// "FooCommand" => "foo"
this.name = this.constructor.name.replace(/Command$/, "").toLowerCase(); //类名称ListCommand
// composed commands are called from other commands, like publish -> version
this.composed = typeof argv.composed === "string" && argv.composed !== this.name;
if (!this.composed) {
// composed commands have already logged the lerna version
log.notice("cli", `v${argv.lernaVersion}`);
}
// launch the command
let runner = new Promise((resolve, reject) => {
// run everything inside a Promise chain
let chain = Promise.resolve();
chain = chain.then(() => {
this.project = new Project(argv.cwd);
});
chain = chain.then(() => this.configureEnvironment()); //配置环境变量
chain = chain.then(() => this.configureOptions()); //配置参数
chain = chain.then(() => this.configureProperties()); //配置属性
chain = chain.then(() => this.configureLogging()); //配置日志
chain = chain.then(() => this.runValidations()); //配置变量
chain = chain.then(() => this.runPreparations()); //配置预检查内容
chain = chain.then(() => this.runCommand()); //核心执行代码
chain.then(
result => {
warnIfHanging();
resolve(result);
},
err => {
if (err.pkg) {
// Cleanly log specific package error details
logPackageError(err, this.options.stream);
} else if (err.name !== "ValidationError") {
// npmlog does some funny stuff to the stack by default,
// so pass it directly to avoid duplication.
log.error("", cleanStack(err, this.constructor.name));
}
// ValidationError does not trigger a log dump, nor do external package errors
if (err.name !== "ValidationError" && !err.pkg) {
writeLogFile(this.project.rootPath);
}
warnIfHanging();
// error code is handled by cli.fail()
reject(err);
}
);
});
// passed via yargs context in tests, never actual CLI
/* istanbul ignore else */
if (argv.onResolved || argv.onRejected) {
runner = runner.then(argv.onResolved, argv.onRejected);
// when nested, never resolve inner with outer callbacks
delete argv.onResolved; // eslint-disable-line no-param-reassign
delete argv.onRejected; // eslint-disable-line no-param-reassign
}
// "hide" irrelevant argv keys from options
for (const key of ["cwd", "$0"]) { // argv中注入"cwd", "$0"两个属性
Object.defineProperty(argv, key, { enumerable: false });
}
Object.defineProperty(this, "argv", { //常规操作,注册this.argv
value: Object.freeze(argv),
});
Object.defineProperty(this, "runner", { //注册this.runner
value: runner,
});
}
}
阅读Command核心代码
- 如果子类中没有定义
initialize和execute方法就直接报错了
runCommand() {
return Promise.resolve()
.then(() => this.initialize())
.then(proceed => {
if (proceed !== false) {
return this.execute();
}
// early exits set their own exitCode (if non-zero)
});
}
initialize() {
throw new ValidationError(this.name, "initialize() needs to be implemented.");
}
execute() {
throw new ValidationError(this.name, "execute() needs to be implemented.");
}
initialize() { //定义输出的内容
let chain = Promise.resolve();
chain = chain.then(() => getFilteredPackages(this.packageGraph, this.execOpts, this.options));
chain = chain.then(filteredPackages => {
this.result = listable.format(filteredPackages, this.options); //输出的内容格式化为字符串
});
return chain;
}
execute() {
// piping to `wc -l` should not yield 1 when no packages matched
if (this.result.text.length) {
output(this.result.text); //终端输出的内容
}
this.logger.success(
"found",
"%d %s",
this.result.count,
this.result.count === 1 ? "package" : "packages"
);
}
Lerna项目本地调试另外一种方法
正常我们是使用npm link进行本地调试,如果包很多的话就比较混乱了,而且每次发布完成后都需要npm unlink ;
发布的时候Lerna使用publish命令参数将本地链接,解析为线上链接。

使用file的方法进行本地调试
在之前lerna-repo项目中;
packages/core
packages/utils
在core中引用‘@rainbow-cli-dev/utils’依赖
"dependencies": {
"@rainbow-cli-dev/utils": "file:../utils",
},
在core目录npm i就会看到core目录的node_modules下创建了"@rainbow-cli-dev/utils"软链接。也可以正常使用依赖。
yargs脚手架用法
npm i yargs
#!/usr/bin/env node
const yargs = require("yargs/yargs");
const dedent = require("dedent");
const { hideBin } = require("yargs/helpers");
const cli = yargs(hideBin(process.argv))
// dedent去除缩进
cli
.strict()
.usage("Usage: $0 <command> [options]")
.demandCommand(
1,
"A command is required. Pass --help to see all available commands and options."
)
.recommendCommands()
.alias("h", "help")
.alias("v", "version")
// 定义多个option参数
.options({
debugger: {
defaultDescription: "",
describe: "bootstrap debugger mode",
type: "boolean",
alias: "d",
},
})
// 注册单个option参数
.option("register", {
describe: "Define Global Option",
type: "string",
alias: "r",
})
.option("ci", {
type: "boolean",
hidden: true,
})
// 定义命令分组
.group(["debugger"], "Dev Options")
.group(["register"], "Extra Options")
.wrap(cli.terminalWidth()).epilogue(dedent`
When a command fails, all logs are written to lerna-debug.log in the current working directory.
For more information, find our manual at https://github.com/lerna/lerna
`).argv;
输入 rainbow-test -h
拿到输出
rainbow@MacBook-Pro-673 rainbow-test % rainbow-test -h
Usage: rainbow-test <command> [options]
Dev Command
-d, --debugger bootstrap debugger mode [布尔]
选项:
-r, --register Define Global Option [字符串]
-h, --help 显示帮助信息 [布尔]
-v, --version 显示版本号 [布尔]
When a command fails, all logs are written to lerna-debug.log in the current working directory.
For more information, find our manual at https://github.com/lerna/lerna
rainbow@MacBook-Pro-673 rainbow-test % rainbow-test -h
Usage: rainbow-test <command> [options]
Dev Options
-d, --debugger bootstrap debugger mode [布尔]
Extra Options
-r, --register Define Global Option [字符串]
选项:
-h, --help 显示帮助信息 [布尔]
-v, --version 显示版本号 [布尔]
When a command fails, all logs are written to lerna-debug.log in the current working directory.
For more information, find our manual at https://github.com/lerna/lerna