nodejs链接、包管理工具、多包管理以及Lerna 工具的使用
1. 概述
2. 硬链接与软链接及其应用
2.1 硬链接与软链接的概念
硬链接和软链接都是用于在文件系统中创建链接的方式,它们可以让多个文件名指向同一个文件从而节省磁盘空间。
2.1.1 硬链接
硬链接是指多个文件名指向同一个文件的链接方式,而软链接是指一个特殊的文件,它的内容是另一个文件的路径名,类似于Windows中的快捷方式。硬链接和原文件是同一个文件,它们的 inode 号相同,因此修改其中一个文件,另一个文件也会被修改。
关于 inode
inode (索引节点,index node)是源于类Unix文件系统中的一种数据结构,每个索引节点保存了文件系统中的一个文件系统对象的元信息数据,但不包括数据内容或者文件名。Linux文件系统使用索引节点来记录文件信息,作用类似于Windows下的文件分配表。一个文件系统维护了一个索引节点的数组,每个文件或目录都与索引节点数组中的唯一一个元素对应。
2.1.2 软链接
软链接是一个指向原文件的符号链接,它的inode号与原文件不同,因此修改原文件不会影响软链接,反之亦然。
2.1.3 通过命令创建链接
2.1.3.1 Windows
在Windows上,可以使用 mklink 命令来创建 硬链接 和 软链接。mklink 命令的格式如下:
mklink [[/D] | [/H] | [/J]] Link Target /D 创建目录符号链接。默认为文件 符号链接。 /H 创建硬链接而非符号链接。 /J 创建目录联接。 Link 指定新的符号链接名称。 Target 指定新链接引用的路径 (相对或绝对)。
注意,在 Windows11 中已经不再保留原 cmd ,并且默认终端换成了 powershell。如果你想在Powershell上直接使用传统的 cmd 命令是会提示无法识别的,这是你可以在 powershell 中输入 cmd 命令以运行
cmd.exe
,这是以一个阉割的版本,可以运行传统的 Windows 命令。更好的方法是使用 Powershell (Windows Powershell 或 Microsoft Powershell,后者是独立安装版,时时有最新的版本),Powershell 是一个强大的脚本语言,即支持shell命令,也可以使用 函数、类、对象、枚举等编程语言来实现功能,并且(Microsoft Powershell)支持跨操作系统平台。关于如何使用 Powershell 创建链接可以参考 2.1.6 通过 Powershell 编程创建链接。
2.1.4.2 Linux
1. 命令语法形式
在Linux上,可以使用ln命令来创建硬链接和软链接。默认情况下创建硬链接,而使用 --symbolic
创建符号链接。 该命令的格式有四种形式:
形式1
ln [OPTION]... [-T] TARGET LINK_NAME
创建一个名为LINK_NAME的目标链接。
形式2
ln [OPTION]... TARGET
创建一个指向当前目录中的 TARGET 的链接
形式3
ln [OPTION]... TARGET... DIRECTORY
创建指向 DIRECTORY 中每个 TARGET 的链接。
形式4
ln [OPTION]... -t DIRECTORY TARGET...
创建指向 DIRECTORY 中每个 TARGET 的链接。
2. 命令的选项
默认情况下:
- 每个 TARGET (新链接的名称) 不应该已经存在。
- 创建硬链接时,每个目标都必须存在。
- 软链接(符号链接)可以保存任意文本;
- 如果以后解析,相对链接将根据其父目录进行解释。
长选项的强制参数对于短选项也是强制的。
选项 | 描述 |
--backup[=CONTROL] |
备份每个现有的目标文件 |
-b |
类似于 --backup 但不接受参数 |
-d , -F , --directory |
允许超级用户尝试硬链接目录(注意:由于系统限制,即使是超级用户也可能会失败) |
-f , --force |
删除现有的目标文件 |
-i , --interactive |
提示是否删除目的地 |
-L , --logical |
取消对符号链接 TARGET 的引用 |
-n , --no-dereference |
在以下情况下,将LINK_NAME 视为普通文件。这是一个指向目录的符号链接 |
-P , --physical |
制作直接指向符号链接的硬链接 |
-r , --relative |
创建相对于链接的符号链接location |
-s , --symbolic |
制作符号链接,而不是硬链接 |
-S , --suffix=SUFFIX |
覆盖通常的备份后缀 |
-t , --target-directory=DIRECTORY |
指定要创建链接的 DIRECTORY |
-T , --no-target-directory |
始终将LINK_NAME视为普通文件 |
-v , --verbose |
打印每个链接文件的名称 |
--help |
显示此帮助并退出 |
--version |
输出版本信息并退出 |
除非使用 --suffix
或 SIMPLE_BACKUP_SUFFIX
设置,否则备份后缀为 ‘~’。
可以通过 --backup
选项或通过选择版本控制方法
可以通过 --backup
选项或通过选择版本控制方法
VERSION_CONTROL 环境变量。以下是这些值:
- none, off 永远不要进行备份(即使给出了
--backup
) - numbered, t 制作 numbered 备份
- existing, nil 如果存在带编号的备份,则 numbered,否则 simple
- simple, never 总是做 simple 备份
2.1.4 通过 Python 编程创建链接
2.1.4.1 创建硬链接
os.link()函数可以用于创建硬链接,它接受两个参数,第一个参数是源文件的路径,第二个参数是目标文件的路径。
注意:源文件和目标文件必须位于同一个文件系统中,否则会抛出OSError异常。
import os os.link('/path/to/source/file', '/path/to/target/file')
2.1.4.2 创建软链接
os.symlink()函数可以用于创建软链接,它接受两个参数,第一个参数是源文件的路径,第二个参数是目标文件的路径。
注意:目标文件必须不存在,否则会抛出OSError异常。
import os os.symlink('/path/to/source/file', '/path/to/target/file')
2.1.5 通过 NodeJS 编程创建链接
2.1.5.1 创建硬链接
fs.link()函数可以用于创建硬链接,它接受两个参数,第一个参数是源文件的路径,第二个参数是目标文件的路径。
注意:源文件和目标文件必须位于同一个文件系统中,否则会抛出Error异常。
const fs = require('fs') fs.link('/path/to/source/file', '/path/to/target/file', (err) => { if (err) throw err })
2.1.5.2 创建软链接
fs.symlink()函数可以用于创建软链接,它接受三个参数,第一个参数是源文件的路径,第二个参数是目标文件的路径,第三个参数是链接类型,可以是’file’或’dir’。
注意:目标文件必须不存在,否则会抛出Error异常。
const fs = require('fs') fs.symlink('/path/to/source/file', '/path/to/target/file', 'file', (err) => { if (err) throw err })
2.1.6 通过 Powershell 编程创建链接
2.1.6.1 创建硬链接
New-Item
cmdlet 可以用于创建硬链接,它接受两个参数,-ItemType 'HardLink’表示创建硬链接,-Path表示目标文件的路径,-Value表示源文件的路径。
New-Item -ItemType 'HardLink' -Path '/path/to/target/file' -Value '/path/to/source/file'
2.1.6.2 创建软链接
New-Item
命令还可以用于创建软链接,它接受两个参数,-ItemType 'SymbolicLink’表示创建软链接,-Path表示目标文件的路径,-Value表示源文件的路径。
New-Item -ItemType 'SymbolicLink' -Path '/path/to/target/file' -Value '/path/to/source/file'
2.1.6 通过 dart 编程创建链接
2.1.7.1 创建硬链接
Link.create()
函数可以用于创建硬链接,它接受两个参数,第一个参数是源文件的路径,第二个参数是目标文件的路径。
注意:源文件和目标文件必须位于同一个文件系统中,否则会抛出
FileSystemException
异常。
import 'dart:io'; Link.create('/path/to/target/file', '/path/to/source/file');
2.1.7.2 创建软链接
Link.createSymbolic()
函数可以用于创建软链接,它接受两个参数,第一个参数是源文件的路径,第二个参数是目标文件的路径。
注意:目标文件必须不存在,否则会抛出
FileSystemException
异常。
import 'dart:io'; Link.createSymbolic('/path/to/target/file', '/path/to/source/file');
3 包管理工具的迭代:npm、yarn 到 pnpm
3.1 从 npm 到 yarn
3.2 从 yarn 到 pnpm
4. 多包管理(Monorepo)及其解决方案
4.1 polyrepo 的概念与缺点
polyrepo 是当前开发应用程序的标准方式:每个团队、应用程序或项目的存储库。通常,每个存储库都有一个生成项目和简单的生成管道。
图片引用-polyrepo-practice1
polyrepo做事方式的原因在于 团队自治,也就是团队希望自行决定他们将使用哪些库、何时部署其应用或库,以及谁可以参与或使用其代码。这种自治是由隔离提供的,而隔离会损害协作。
图片引用-monorepo-polyrepo2
更具体地说,多存储库环境下 Polyrepo 有这些的常见缺点:
- 繁琐的代码共享
若要跨存储库共享代码,可能需要为共享代码创建存储库。现在,您必须设置工具和 CI 环境,将提交程序添加到存储库,并设置包发布,以便其他存储库可以依赖它。让我们不要开始协调跨存储库的第三方库的不兼容版本… - 大量代码重复
没有人愿意经历设置共享存储库的麻烦,因此团队只需在每个存储库中编写自己的通用服务和组件实现。这浪费了前期时间,但也增加了维护、安全和质量控制的负担,因为组件和服务发生了变化。 - 对共享库和使用者进行成本高昂的跨存储库更改
考虑共享库中的关键错误或重大更改:开发人员需要设置其环境以将更改应用于具有断开连接的修订历史记录的多个存储库。更不用说版本控制和发布包的协调工作了。 - 工具不一致
每个项目都使用自己的一组命令来运行测试、生成、服务、检查、部署等。不一致会产生记住从一个项目到另一个项目使用哪些命令的心理开销。
monorepo 是包含多个不同项目的单个存储库,具有明确定义的关系。
npm、yarn、pnpm 都可以进行多包管理。如前文所介绍,npm 和 yarn 都是使用 软链接 来实现的,而pnpm则使用硬链接和 符号链接(硬链接) 来实现的。因此相比之下 pnpm 可以 显著减少磁盘空间的使用,同时也可以 加快安装和更新的速度。
4.2 为什么使用 monorepo
polyrepo 的反面就是 monorepo 。
Monorepo 将多个相关的项目放在同一个代码仓库中进行管理的方式。这种方式可以减少代码重复,方便代码共享和重构,同时也可以提高开发效率和代码质量。
图片引用-monorepo-polyrepo2
相比于 polyrepo, monorepo 具有以下特点:
- 创建新项目无开销
使用现有的 CI 设置,如果所有使用者都在同一存储库中,则无需发布版本化的包。 - 跨项目的原子提交
每次提交时,一切都可以协同工作。当您在同一提交中修复所有内容时,没有重大更改这样的事情。 - 一切的一个版本
无需担心不兼容,因为项目依赖于第三方库的冲突版本。 - 开发人员移动性
获得构建和测试使用不同工具和技术编写的应用程序的一致方法。开发人员可以放心地为其他团队的应用程序做出贡献,并验证他们的更改是否安全。
Monorepos 有很多优点,但要使它们工作,您需要拥有合适的工具。随着工作空间的增长,这些工具必须帮助您保持快速、易懂和易于管理。 Monorepo工具应提供以下功能:
- 本地计算缓存
存储和重播文件和处理任务输出的能力。在同一台计算机上,您永远不会两次构建或测试同一事物。
图片引用-本地计算缓存3 - 本地任务编排
以正确的顺序并行运行任务的能力。所有列出的工具都可以以大致相同的方式完成此操作,除了 Lerna,它更受限制。
图片引用-本地任务编排4 - 分布式计算缓存
在不同环境之间共享缓存项目的能力。这意味着您的整个组织(包括 CI 代理)永远不会两次构建或测试相同的东西。
图片引用-monorepo-polyrepo2
图片引用-分布式计算缓存5 - 分布式任务执行
能够在多台计算机上分发命令,同时在很大程度上保留在单台计算机上运行命令的开发人体工程学。
图片引用-分布式任务执行6 - 透明的远程执行
在本地开发时在多台计算机上执行任何命令的能力。 - 检测受影响的项目/包
确定更改可能影响的内容,以仅运行生成/测试受影响的项目。
图片引用-检测受影响的项目7 - 工作空间分析
无需额外配置即可理解工作区的项目图的能力。
图片引用-工作空间分析8 - 依赖关系图可视化
可视化项目和/或任务之间的依赖关系。可视化是交互式的,这意味着您可以搜索,过滤,隐藏,聚焦/突出显示和查询图形中的节点。 - 源代码共享
便于共享离散的源代码片段。
https://monorepo.tools/images/source-code-sharing.svg - 一致的工具
无论您使用什么来开发项目,该工具都可以帮助您获得一致的体验:不同的JavaScript框架,Go,Rust,Java等。
换句话说,该工具以相同的方式处理不同的技术。
例如,该工具可以分析package.json和JS/TS文件,以确定JS项目部门,以及如何构建和测试它们。但它会分析 Cargo.toml 文件以对 Rust 执行相同的操作,或者分析 Gradle 文件以对 Java 执行相同的操作。这要求工具可插拔。
图片引用-一致的工具9 - 代码生成
对生成代码的本机支持
图片引用-代码生成10 - 项目约束和可见性
支持定义规则以约束存储库中的依赖关系。例如,开发人员可以将某些项目标记为其团队的私有项目,这样其他人就无法依赖它们。开发人员还可以根据所使用的技术(例如,React或Nest.js)标记项目,并确保后端项目不会导入前端项目。
图片引用-项目约束和可见性11
4.3 工作区的概念
工作区是一个通用术语,不论你使用哪一个包管理工具来管理你的 node 模块,都绕不开 工作区(workspace,工作空间)的概念。
使用多包管理的项目
从功能上看,工作区支持从单个 nodeJS 的 顶级根模块 中管理本地文件系统中的多个包。弥补了更简化的工作流程处理 来自本地文件系统的链接包。自动化链接过程 作为并避免手动使用 以添加对应符号链接到当前文件夹的包的引用。
工作区通常通过根项目 package.json
文件的属性来定义,比如:
{ "name": "example", "workspaces": [ "packages/a" ] }
在上面的示例中,当前工作目录 .
包含一个名为packages/a
的文件夹,该文件夹内部包含一个,定义一个Node.js包,例如:
└─<packages> ├─package.json ├─pnpm-lock.yaml ├─<pck1> | ├─package.json | ... └─<pck2> ├─package.json ...
5. 使用 lerna 进行多包管理
5.1 常见的 monorepo 工具
目前有很多用于 monorepo 的工具,比如:
- Bazel(由Google);
- Rush(由Microsoft);
- Lage(由Microsoft)
- Gradle Build Tool(由Gradle,Inc)
- Lerna
- Nx(由Nrwl)
- Pants(由Pants Build社区);
- Turborepo(由Vercel).
5.1 lerna 安装与初始化项目
Lerna 是一个用于多包管理的工具,它可以在pnpm工作区中使用,以获得pnpm和Lerna的全部好处。你需要先全局安装 lerna :
npm i lerna -g
然后可以使用 lerna 初始化你的项目:
lerna init
你大概会看到这样的信息:
lerna notice cli v6.6.1 lerna info Initializing Git repository lerna info Creating .gitignore lerna info Creating package.json lerna info Creating lerna.json lerna info Creating packages directory lerna success Initialized Lerna files lerna info New to Lerna? Check out the docs: https://lerna.js.org/docs/getting-started
这些信息解释了 lerna init
都为我们完成了哪些工作:
- 一个初始化的 git 本地仓库;
- 一个
.gitignore
文件; - 一个
package.json
文件; - 一个
lerna.json
文件; - 一个
packages
目录。 - 其中,
lerna.json
文件初始化后的内容是这样的:
{ "$schema": "node_modules/lerna/schemas/lerna-schema.json", "useWorkspaces": true, "version": "0.0.0" }
这是 lerna 的一些配置,比如是否使用工作区。运行时,Lerna 将工作区配置为使用 NPM/YARN/PNPM 工作区,这是用于本地引用包的内置解决方案。
- 其中,
packages
文件夹在之后用于存放所有 node 项目/包,他们都有自己独立的package.json
文件,只不过这些项目的node_modules
中的模块实际的存储是通过符号连接的方式使用根项目node_modules
下对应的模块目录。 - 其中, 初始化的
package.json
是这样的:
{ "name": "root", "private": true, "workspaces": [ "packages/*" ], "devDependencies": { "lerna": "^6.6.1" } }
从这个文件可以看出,lerna 默认将 packages
目录下的所有目录置于工作区中。
接着,你可以使用 npm/yarn/pnpm 安装依赖:
npm install # or yarn # or pnpm install
5.2 lerna 命令
5.2.1 lerna publish
在当前项目中发布包
lerna publish # 发布自上一次发布以来已更改的包 lerna publish from-git # 显式发布在当前提交中标记的包 lerna publish from-package # 明确发布注册表中没有最新版本的包
运行时,此命令执行以下操作之一:
- 发布自上次发布以来更新的包(在后台调用lerna版本)。
- 这是lerna 2.x的遗留行为
- 发布在当前提交中标记的包(from-git)。
- 在最新提交时发布注册表中没有的版本的包(from-package)。
- 发布在之前提交中更新的包(及其依赖项)的非版本化“canary”版本。
Lerna不会发布标记为private的包(
private
在package.json
中为true)。这与npm发布的行为一致。
该命令具有以下选项:
--canary
选项
当使用这个标志运行时,lerna publish以更细粒度方式(每次提交)发布包。在发布到npm之前,它通过获取当前版本、将其转换到下一个次要版本、添加提供的meta后缀(默认为alpha)并附加当前git sha(例如:1.0.0
变成1.1.0-alpha.0+81e3b443
)来创建新的版本标记。
如果您已经从CI中的多个活动开发分支发布了canary版本,那么建议在每个分支的基础上定制--preid
和--dist-tag <tag>
,以避免版本冲突。
--contents <dir>
选项
指定要发布的子目录。必须应用于所有包,并且必须包含package.json文件。包生命周期仍将在原始叶目录中运行。您可能应该使用其中一个生命周期(prepare
、prepublishOnly
或prepack
)来创建子目录等。
如果你对不必要的复杂出版感兴趣,这会给你带来快乐。
例如:
lerna publish --contents dist #发布每个Lerna管理的叶包的“dist”子文件夹
注意:您应该等到
postpublish
生命周期阶段(根或叶)来清理这个生成的子目录,因为生成的package.json是在包上传期间使用的(在postpack
之后)。
--dist-tag <tag>
选项
使用此标志运行时,lerna publish
将使用给定的npm dist-tag
(默认为latest
)发布到npm。
此选项可用于在非最新发行标签下发布 prerelease
或 beta
版本,帮助避免自动升级到预发布质量的代码。
注意:
latest
标记是用户运行npm install my-package
时使用的标记。要安装不同的标记,用户可以运行npm install my-package@prerelease
。
--force-publish
选项
与 --canary
一起使用,发布monorepo中所有包的canary版本。当您需要在最近一次提交中更改的基础上发布 canary 版本的包时,这个标志会很有帮助。例如:
lerna publish --canary --force-publish
--git-head <sha>
选项
Explicit SHA to set as gitHead on manifests when packing tarballs, only allowed with from-package positional.
For example, when publishing from AWS CodeBuild (where git is not available), you could use this option to pass the appropriate environment variable to use for this package metadata:
lerna publish from-package --git-head ${CODEBUILD_RESOLVED_SOURCE_VERSION}
在所有其他情况下,该值来自本地 git
命令。
--graph-type <all|dependencies>
选项
Set which kind of dependencies to use when building a package graph. The default value is dependencies, whereby only packages listed in the dependencies section of a package’s package.json are included. Pass all to include both dependencies and devDependencies when constructing the package graph and determining topological order.
When using traditional peer + dev dependency pairs, this option should be configured to all so the peers are always published before their dependents.
lerna publish --graph-type all
Configured via lerna.json:
{ "command": { "publish": { "graphType": "all" } } }
--ignore-scripts
选项
通过后,此标志将禁止在lerna发布期间运行生命周期脚本。
--ignore-prepublish
选项
通过后,此标志将禁止在lerna发布期间运行不推荐使用的预发布脚本。
--include-private
选项
指示标有"private": true
表示应该发布。因为npm publish
拒绝发布任何带有"private": true
的包,所以Lerna在发布之前会删除该属性。
请注意,这与私有范围的包不同,私有范围的包在其package.json
中没有"private": true
,旨在发布,默认情况下由lerna publish
包含。
⚠️警告:标有
"private": true
的包不打算被公布,在npm package.json文档中有详细说明。该选项的目的是允许将将来公开的包发布到本地注册中心进行测试。
例如:
lerna publish --include-private my-private-package lerna publish --include-private my-private-package my-other-private-package
--legacy-auth
选项
当发布需要身份验证的包时,但您正在使用内部托管的NPM注册表,该注册表仅使用旧版Base64版本的用户名:密码。这与NPM 的 publish _auth
标志相同。
例如:
lerna publish --legacy-auth aGk6bW9t
--no-git-reset
选项
默认情况下,lerna publish确保对工作树的任何更改都已重置。
要避免这种情况,请传递 --no-git-reset
。当作为CI管道的一部分与 --canary
标志一起使用时,这尤其有用。例如,被删除的 package.json
版本号可能需要在后续的CI管道步骤中使用(比如Docker构建)。
例如:
lerna publish --no-git-reset
--no-granular-pathspec
选项
By default, lerna publish will attempt (if enabled) to git checkout only the leaf package manifests that are temporarily modified during the publishing process. This yields the equivalent of git checkout – packages/*/package.json, but tailored to exactly what changed.
如果你知道你需要不同的行为,你就会明白:传入 --no-granular-pathspec
使git命令字面上git checkout -- .
。通过选择这个pathspec
,你必须适当地忽略所有有意未版本化的内容。
这个选项在 lerna.json
中配置是最有意义的,因为你真的不想把它弄糟:
{ "version": "independent", "granularPathspec": false }
根级配置是有意的,因为这也包括lerna版本中的同名选项。
--verify-access
选项
过去,lerna
试图通过使用给定的令牌执行一些抢先的npm API请求来快速解决授权/身份验证问题。然而,目前npm支持多种类型的令牌,并且它们具有不同级别的访问权限,因此对于这种先发制人的检查,没有一刀切的解决方案,更合适的做法是允许对npm的请求失败,并针对给定令牌显示适当的错误。因此,默认情况下会禁用已过时的 --verify-access
行为,并且可能会在未来的主要版本中删除。
但是现在,如果您传递这个标志,您可以选择遗留行为,lerna将在尝试发布任何包之前先执行这个验证。
在以下情况下,您不应使用此选项:
- 您正在使用不支持
npm access ls-packages
的第三方注册表; - 您正在使用没有读取权限的身份验证令牌,例如npm自动化访问令牌
--otp
选项
与同名的 lerna version
选项不同,该选项仅适用于- canary版本计算。
例如:
lerna publish --canary # 使用下一个语义预发布版本,例如 # 1.0.0 => 1.0.1-alpha.0 lerna publish --canary --preid next # 使用带有特定预发布标识符的下一个语义预发布版本,例如 # 1.0.0 => 1.0.1-next.0
当使用此标志运行时,lerna publish - canary
将使用指定的预发布标识符递增premajor
、preminor
、prepatch
或预发布semver bumps。
--preid
选项
--pre-dist-tag <tag>
选项
与--dist-tag
的作用相同,只是仅适用于与预发行版本一起发布的软件包。如:
lerna publish --pre-dist-tag next
--registry <url>
选项
使用此标志运行时,转发的npm命令将使用您的软件包的指定注册表。
如果您不想在所有 package.json
文件中单独地显式设置注册表配置,例如使用私有注册表时,这很有用。
--tag-version-prefix
选项
该选项允许提供自定义前缀,而不是默认前缀:v
。
请记住,如果拆分lerna version
和lerna publish
,您需要将它传递给这两个命令:
# locally lerna version --tag-version-prefix='' # on ci lerna publish from-git --tag-version-prefix=''
你还可以在lerna.json
的根级别进行配置,同等地应用于这两个命令:
{ "tagVersionPrefix": "", "packages": ["packages/*"], "version": "independent" }
--temp-tag
选项
通过后,此标志将改变默认的发布过程,首先将所有已更改的包发布到临时dist-tag (lerna-temp ),然后将新版本移动到由- dist-tag配置的 --dist-tag
(默认为latest
)。
这通常是不必要的,因为默认情况下,Lerna将按照拓扑顺序(所有依赖项在依赖项之前)发布包。
--yes
选项
使用该标志运行时,lerna publish将跳过所有确认提示。在持续集成(CI) 中用于自动回答发布确认提示。)中用于自动回答发布确认提示。
lerna publish --canary --yes # 跳过 Are you sure you want to publish the above changes?(您确定要发布上述更改吗?)
--summary-file <dir>
选项
当使用这个标志运行时,在成功发布所有包之后,将会生成一个json 摘要报告(参见下面的示例)。
[ { "packageName": "package1", "version": "v1.0.1-alpha" }, { "packageName": "package2", "version": "v2.0.1-alpha" } ]
例如:
#将在根目录下创建一个摘要文件,即 `./lerna-publish-summary.json` lerna publish --canary --yes --summary-file # 将在提供的目录中创建一个摘要文件,即 `./some/other/dir/lerna-publish-summary.json` lerna publish --canary --yes --summary-file ./some/other/dir
5.2.2 lerna version
例如:
lerna version 1.0.1 # explicit lerna version patch # semver keyword lerna version # select from prompt(s)
运行时,该命令执行以下操作:
- 标识自上一个标记版本以来已更新的软件包。
- 提示输入新版本。
- 修改包元数据以反映新版本,在根和每个包中运行适当的生命周期脚本。
- 提交这些更改并标记提交。
- 推送到git远程仓库。
更多信息参考 https://github.com/lerna/lerna/tree/main/libs/commands/version#readme
5.2.3 lerna bootstrap
该命令将本地软件包链接在一起,并安装剩余的软件包依赖项。例如:
lerna bootstrap
运行时,该命令将:
1.npm安装每个包的所有外部依赖项。
2.将所有相互依赖的Lerna包符号链接在一起。
3.npm在所有引导程序包中运行prepublish(除非传递了- ignore-prepublish)。
4.npm在所有引导程序包中运行准备。
lerna bootstrap接受所有过滤器标志(filter flags)。
通过将额外的参数放在以下--
位置之后,将它们传递给npm客户端:
lerna bootstrap -- --production --no-optional
也可以在lerna.json
中配置:
{ ... "npmClient": "yarn", "npmClientArgs": ["--production", "--no-optional"] }
–hoist [glob] 选项
在repo根安装匹配glob
的外部依赖项,以便它们可用于所有软件包。这些依赖项中的任何二进制文件都将被链接到依赖包node_modules/.bin/
目录,以便它们可用于npm脚本。如果该选项存在,但没有给定glob
,则默认值为**
(提升所有内容)。此选项仅影响bootstrap
命令。例如:
lerna bootstrap --hoist
有关--hoist
的背景资料,请参见 hoist文档。
注意:如果软件包依赖于外部依赖项的不同版本,将会提升最常用的版本,并发出警告。
--hoist
与file:specifiers
不兼容。使用其中之一。
从3.18.0版开始,--hoist
不再接受多个字符串值。请使用:
- a. 用引号将字符串值括起来:
lerna bootstrap --hoist "{rollup,postcss-cli,webpack-cli,babel-loader,npm-run-all}"
- b. 指定
lerna.json
中的值列表:
{ "command": { "bootstrap": { "hoist": [ "rollup", "postcss-cli", "webpack-cli", "babel-loader", "npm-run-all" ] } }, ... }
–strict 选项
lerna bootstrap --hoist --strict
–nohoist [glob]
不要在repo根安装与 glob
匹配的外部依赖项。这可以用于选择不提升某些依赖项。
lerna bootstrap --hoist --nohoist=babel-*
–ignore 选项
lerna bootstrap --ignore component-*
与 bootstrap
命令一起使用时,还可以在lerna.json
中的command.bootstrap.ignore
键下设置--ignore
标志。命令行标志将优先于此选项。
例如:
{ "version": "0.0.0", "command": { "bootstrap": { "ignore": "component-*" } } }
提示:
glob
匹配package.json
中定义的包名,而不是包所在的目录名。
–ignore-prepublish 选项
跳过引导程序包中默认运行的预发布 生命周期 脚本。
例如:
lerna bootstrap --ignore-prepublish
请注意,这个 生命周期 已被废弃了,很可能会在Lerna的下一个主要版本中被删除。
–ignore-scripts 选项
跳过任何通常在引导包中运行的生命周期脚本。
lerna bootstrap --ignore-scripts
–registry <url> 选项
使用此标志运行时,转发的npm命令将使用您的软件包的指定注册表。
如果您不想在所有 package.json
文件中单独地显式设置注册表配置,例如使用私有注册表时,这很有用。
–npm-client <client> 选项
必须是知道如何安装npm软件包依赖项的可执行文件。默认的 --npm-client
是 npm
。
lerna bootstrap --npm-client=yarn
也可以在 lerna.json
文件中配置:
{ ... "npmClient": "yarn" }
–use-workspaces 选项
支持与 Yarn Workspaces 集成(从yarn@0.27+开始提供)。数组中的值是Lerna将操作委托给Yarn的命令(目前仅用于引导)。如果--use-workspaces
为 true,则包将被package.json/workspaces
,中的值覆盖,并且--ignore
和--scope
都将被忽略。也可以在lerna.json
中配置:
{ ... "npmClient": "yarn", "useWorkspaces": true }
根级别的 package.json
还必须包含一个 workspaces
数组:
{ "private": true, "devDependencies": { "lerna": "^2.2.0" }, "workspaces": ["packages/*"] }
这个列表与lerna的packages config(一个用 package.json
匹配目录的 glob 列表)大致相似,只是它不支持递归glob(“**
”,也称为“globstars
”)。
–no-ci 选项
当使用默认的 --npm-client
时,lerna bootstrap
将调用 npm ci
,而不是在 CI(持续集成) 环境中安装npm。要禁用此行为,请传递 --no-ci
:
lerna bootstrap --no-ci
要在本地安装过程中强制使用它(它不会自动启用),请传递 --ci
:
lerna bootstrap --ci
这对于 “clean” 重新安装或全新克隆后的初始安装非常有用。
–force-local 选项
传递时,此标志会导致 bootstrap
命令始终对本地依赖项进行符号链接,而不考虑匹配的版本范围。
lerna bootstrap --force-local
publishConfig.directory 字段
这个非标准字段允许您自定义将成为符号链接的源目录的符号链接子目录,就像如何使用已发布的包一样。
例如:
"publishConfig": { "directory": "dist" }
在这个示例中,当这个包被引导和链接时,dist目录将是源目录(例如 package-1/dist => node_modules/package-1
)。
5.2.4 lerna list
列出本地包。
list 子命令是几个方便的快捷键的别名(类似于npm ls):
lerna ls
: 与lerna list
相同,它本身类似于ls命令lerna ll
: 相当于lerna ls -l
,显示长输出lerna la
: 相当于lerna ls -la
,显示所有包(包括私有包)
例如:
lerna ls
Out[]
:
package-1 package-2
–json 选项
将信息显示为JSON数组。例如:
lerna ls --json
Out[]
:
[ { "name": "package-1", "version": "1.0.0", "private": false, "location": "/path/to/packages/pkg-1" }, { "name": "package-2", "version": "1.0.0", "private": false, "location": "/path/to/packages/pkg-2" } ]
可以通过管道连接到json实用程序来挑选单个属性,例如:
lerna ls --json --all | json -a -c 'this.private === true' name package-3
–ndjson 选项
用于 将信息显示为 换行符分隔的JSON。例如:
lerna ls --ndjson
Out[]
:
{"name":"package-1","version":"1.0.0","private":false,"location":"/path/to/packages/pkg-1"} {"name":"package-2","version":"1.0.0","private":false,"location":"/path/to/packages/pkg-2"}
–all 选项(别名:-a
)
显示默认情况下隐藏的私有包。例如:
lerna ls --all
Out[]
:
package-1 package-2 package-3 (private)
–long 选项(别名:-l
)
显示扩展信息。例如:
lerna ls --long
Out[]
:
package-1 v1.0.1 packages/pkg-1 package-2 v1.0.2 packages/pkg-2
例如:
lerna ls -la
Out[]
:
package-1 v1.0.1 packages/pkg-1 package-2 v1.0.2 packages/pkg-2 package-3 v1.0.3 packages/pkg-3 (private)
–parseable 选项(别名:-p
)
显示可分析的输出,而不是列视图。默认情况下,输出的每一行都是包的绝对路径。例如:
lerna ls --parseable
Out[]
:
/path/to/packages/pkg-1 /path/to/packages/pkg-2
例如:
lerna ls -pl
Out[]
:
/path/to/packages/pkg-1:package-1:1.0.1 /path/to/packages/pkg-2:package-2:1.0.2
例如:
lerna ls -pla
Out[]
:
/path/to/packages/pkg-1:package-1:1.0.1 /path/to/packages/pkg-2:package-2:1.0.2 /path/to/packages/pkg-3:package-3:1.0.3:PRIVATE
–toposort 选项
按拓扑顺序(依赖项在依赖项之前)对包进行排序,而不是按目录进行词汇排序。例如:
packages/pkg-1/package.json
中:
"dependencies ": { "pkg-2": "file:../pkg-2" }
lerna ls --toposort
Out[]
:
package-2 package-1
–graph 选项
将依赖图显示为JSON格式的 邻接表。
例如:
lerna ls --graph
Out[]
:
{ "pkg-1": [ "pkg-2" ], "pkg-2": [] } $ lerna ls --graph --all { "pkg-1": [ "pkg-2" ], "pkg-2": [ "pkg-3" ], "pkg-3": [ "pkg-2" ] }
5.2.5 lerna changed
列出自上一个标记版本以来发生变化的本地软件包。
lerna changed
的输出是一个包列表,这些包将成为下一个 lerna version
或 lerna publish
执行的主题。
例如:
lerna changed
Out[]
:
package-1 package-2
注意:
lerna publish
和lerna version
的lerna.json
配置也会影响lerna changed
,例如command.publish.ignoreChanges
。
5.2.6 lerna diff
区别自上一版本以来的所有软件包或单个软件包。其用法如下:
$ lerna diff [package] $ lerna diff # 区分特定的包 $ lerna diff package-name
区别自上一个版本以来的所有软件包或单个软件包。
类似于
lerna changed
。这个命令运行git diff.
。
5.2.7 lerna exec
★5.2.8 lerna run
在每个包中执行任意命令。用法如下:
lerna exec -- <command> [..args] # 在所有包中运行该命令 lerna exec -- rm -rf ./node_modules lerna exec -- protractor conf.js
在每个包中运行任意命令。双破折号(--
)是将虚线标志传递给衍生命令所必需的,但当所有参数都是位置性的时,则不是必需的。
当前包的名称可通过环境变量 LERNA_PACKAGE_NAME
获得:
lerna exec -- npm view \$LERNA_PACKAGE_NAME
也可以通过环境变量 LERNA_ROOT_PATH
运行复杂目录结构中根目录下的脚本:
lerna exec -- node \$LERNA_ROOT_PATH/scripts/some-script.js
filter-options
lerna exec
接受所有的 filter-options,比如:
lerna exec --scope my-component -- ls -la
这些命令是使用给定的并发性并行生成的(除了--parallel
)。输出是通过管道传输的,因此不具有确定性。如果您想在一个又一个包中运行该命令,请像这样使用它:
lerna exec --concurrency 1 -- ls -la
–stream 选项
立即从子进程输出流,以原始包名为前缀。这使得不同封装的输出可以交错。
lerna exec --stream -- babel src -d lib
–parallel 选项
类似于 --stream
,但是完全不考虑并发性和拓扑排序,在所有匹配的包中立即运行给定的命令或脚本,带有前缀的流输出。这是长期运行进程的首选标志,比如 babel src -d lib -w
运行在许多包上。
lerna exec --parallel -- babel src -d lib -w
注意:建议在使用
--parallel
标志时限制该命令的作用域,因为生成许多子进程可能会损害shell的稳定性(例如,或最大文件描述符限制)。你的经历可能不一样。
–no-bail 选项
# 运行命令,忽略非零(错误)退出代码 lerna exec --no-bail <command>
默认情况下,如果任何执行返回非零退出代码,lerna exec
将退出并出错。通过 --no-bail
禁用此行为,在所有包中执行,不管退出代码是什么。
–no-prefix 选项
当输出为流式时,禁用包名前缀(--stream
或--parallel
)。当将结果传送到其他进程(如编辑器插件)时,此选项会很有用。
–profile 选项
分析命令执行并生成一个性能分析文件,可以在基于Chromium的浏览器中使用DevTools进行分析(直接URL: devtools://devtools/bundled/devtools_app.html
)。该配置文件显示了命令执行的时间线,其中每个执行被分配给一个开放的槽。插槽的数量由--concurrency
选项确定,开放插槽的数量由--concurrency
减去正在进行的操作的数量确定。最终结果是命令并行执行的可视化。
性能配置文件输出的默认位置是项目的根目录:
lerna exec --profile -- <command>
注意:Lerna将仅在启用拓扑排序时进行分析(即不使用
--parallel
和--no-sort
)。
–profile-location <location> 选项
您可以为性能配置文件输出提供一个自定义位置。提供的路径将相对于当前工作目录进行解析。例如:
lerna exec --profile --profile-location=logs/profile/ -- <command>
5.2.9 ★lerna init
创建新的Lerna repo或将现有的repo升级到Lerna的当前版本。用法如:
lerna init
Lerna假设repo已经用git init
初始化。
运行时,该命令将:
- 如果尚不存在
lerna
,则将其作为 devDependency 添加到 package.json中。 - 创建一个
lerna.json
配置文件来存储版本号。 - 如果不存在
.gitignore
文件,则生成一个。
–independent 选项
这个标志告诉Lerna使用独立的版本控制模式。例如:
lerna init --independent
–exact 选项
默认情况下,在添加或更新lerna的本地版本时,lerna init
将使用插入符号范围,就像npm install --save-dev lerna
一样。
lerna init --exact
如果要保留 “精确” 比较的 lerna 1.x 行为,请传递此标志。它将配置 lerna.json
,以便对所有后续执行强制执行精确匹配。
{ "command": { "init": { "exact": true } }, "version": "0.0.0" }
★5.2.10 lerna add
向匹配的包添加依赖项。用法格式为:
lerna add <package>[@version] [--dev] [--exact] [--peer]
将本地或远程程序包作为依赖项添加到当前Lerna报告中的程序包。请注意,与 yarn add
或 npm install
相比,一次只能添加一个包。
运行时,该命令将:
- 将包添加到每个适用的包中。适用于未打包但在范围内的产品包
- 包含对其清单文件(
package.json
)的更改的引导程序包
如果没有提供版本说明符,则默认使用最新的dist-tag,就像npm install
一样。
例如:
# 将模块1软件包添加到前缀为“prefix-”的文件夹中的软件包中 lerna add module-1 packages/prefix-* # 将 module-1 安装到 module-2 lerna add module-1 --scope=module-2 # 在 devDependencies 中安装 module-1 到 module-2 lerna add module-1 --scope=module-2 --dev # 在 peerDependencies 中安装 module-1 到 module-2 lerna add module-1 --scope=module-2 --peer # 在除 module-1 之外的所有模块中安装 module-1 lerna add module-1 # 在所有模块中安装 babel-core lerna add babel-core
–dev 选项
将新包添加到 devDependencies
,而不是 dependencies
。
–exact 选项
添加具有确切版本(例如1.0.1
)的新软件包,而不是默认的^
semver range(例如^1.0.1
)。例如:
lerna add --exact
–peer 选项
将新包添加到 peerDependencies
,而不是 dependencies
。
–registry <url> 选项
使用自定义注册表安装目标软件包。
–no-bootstrap 选项
跳过链接的 lerna bootstrap
。
★5.2.11 lerna clean
从所有软件包中删除node_modules
目录:
lerna clean
lerna clean
接受所有filter-options,以及 --yes.
。
lerna clean
不会从根node_modules
目录中删除模块,即使您启用了--hoist
选项。
★5.2.12 lerna import
将包导入具有提交历史记录的 monorepo。
lerna import <path-to-external-repository>
将位于 <path-to-external-repository>
的包和提交历史导入到 packages/<directory-name>
中。原始提交作者、日期和消息将被保留。提交将应用于当前分支。
这对于将预先存在的独立软件包收集到一个Lerna repo中非常有用。每次提交都会被修改,以做出相对于包目录的更改。因此,例如,添加 package.json
的提交将改为添加 packages/<directory-name>/package.json
。
注意:如果您在一个新的lerna存储库上导入一个外部存储库,那么一定要记住至少有一个提交。
例如:
# Lerna入门 $ git init lerna-repo && cd lerna-repo $ npx lerna init $ npm install # 添加一个 commit $ git add . $ git commit -m "Initial lerna commit" # 如果没有commit,导入命令将会失败 # 导入其他存储库 $ npx lerna import <path-to-external-repository>
–flatten 选项
当导入带有冲突合并提交的存储库时,导入命令将无法尝试应用所有提交。用户可以使用此标志来请求导入“平面”历史,即,将每次合并提交作为合并引入的单个更改。
lerna import ~/Product --flatten
–dest 选项
导入存储库时,可以通过 lerna.json
中列出的目录来指定目标目录。
lerna import ~/Product --dest=utilities
–preserve-commit 选项
每个git提交都有一个作者和一个提交者(每个都有单独的日期)。通常他们是同一个人(和日期),但是因为lerna import
从外部存储库重新创建每个提交,所以提交者成为当前的git用户(和日期)。这在技术上是正确的,但可能是不可取的,例如,在Github上,如果作者和提交者是不同的人,就会显示他们,这可能会导致混淆历史/对导入的提交进行指责。
启用此选项会保留原始提交者(和提交日期)以避免此类问题。
lerna import ~/Product --preserve-commit
5.2.13 lerna link
将所有相互依赖的包 软链接 在一起:
lerna link
将当前Lerna repo中相互依赖的所有Lerna软件包符号链接在一起。
–force-local 选项
传递时,此标志导致link命令总是对本地依赖项进行 符号链接 ,而不考虑匹配的版本范围。
lerna link --force-local
publishConfig.directory 字段
这个非标准字段允许您自定义将成为符号链接的源目录的符号链接子目录,就像如何使用已发布的包一样,例如:
"publishConfig": { "directory": "dist" }
在此示例中,当链接此程序包时,dist目录将是源目录(例如package-1/dist => node_modules/package-1
)。
5.2.14 lerna create
创建一个新的 lerna 管理的包,用法为:
lerna create <name> [loc]
位置参数:
name
[string] [required] 包名(包括范围),它必须在本地是唯一的并且可以公开获得loc
[string] 自定义包位置,默认为第一个配置的包位置。该位置必须与已配置的软件包目录相匹配。
选项
--access |
使用范围时,设置 publishConfig.access 值 |
[choices: “public”, “restricted”] 、 [default: public] |
--bin |
包有一个可执行文件。用 --bin <executableName> 自定义 |
[default: ] |
--description |
包的描述 | [string] |
--dependencies |
一个包 dependencies 列表 |
[array] |
--es-module |
初始化一个 transpiled ES模块 | |
--homepage |
包的主页,默认为根 pkg.homepage 的子路径 |
[string] |
--keywords |
包关键字列表 | [array] |
--license |
所需的软件包许可证(SPDX identifier) | [default: ISC] |
--private |
将新包设为私有,表示它不应被发布。 | |
--registry |
配置包的publishConfig.registry |
[string] |
--tag |
配置包的 publishConfig.tag |
[string] |
--yes |
跳过所有提示,接受默认值 |
5.2.15 lerna info
打印本地环境信息。info命令打印本地环境信息,这被证明是有用的,尤其是在提交错误报告时。例如:
lerna info
Out[]
:
Environment Info: System: OS: macOS 13.1 CPU: (10) arm64 Apple M1 Max Binaries: Node: 16.15.0 - ~/.volta/tools/image/node/16.15.0/bin/node Yarn: 1.22.18 - ~/.volta/tools/image/yarn/1.22.18/bin/yarn npm: 8.5.5 - ~/.volta/tools/image/node/16.15.0/bin/npm Utilities: Git: 2.37.1 - /usr/bin/git
5.2.16 lerna add-caching
运行设置基本缓存选项的向导:
lerna add-caching
向导提出的每个问题都将告知如何更新nx.json文件。
哪些脚本需要按顺序运行?
在运行脚本本身之前,每个选定的脚本将被标记为运行同名的依赖关系脚本。
如果您将 build
标记为需要拓扑顺序,nx.json
文件将如下所示:
{ "targetDefaults": { "build": { "dependsOn": [ "^build" // 在生成此项目之前,生成所有依赖项 ] } } }
哪些脚本是可缓存的?
每个选中的脚本都将被Lerna缓存。仅选择不依赖于任何外部输入(如网络调用)的脚本。build
和 test
通常是可缓存的。start
和 serve
通常是不可缓存的。有时 e2e
是可缓存的。
如果将 build
标记为可缓存,nx.json
文件将如下所示:
{ "tasksRunnerOptions": { "default": { "runner": "nx/tasks-runners/default", "options": { "cacheableOperations": [ "build" // 缓存 build 脚本 ] } } } }
“[script_name]”脚本会创建任何输出吗?
对于每个选择为可缓存的脚本,向导将要求输出路径。对于构建,这将是构建输出。对于测试,这将是任何覆盖报告。输出路径相对于项目根目录。
如果指定 dist
作为 build
脚本的输出路径,nx.json
文件将如下所示:
{ "targetDefaults": { "build": { "outputs": ["{projectRoot}/dist"] } } }
5.2.17 lerna repair
更新配置文件以匹配当前安装的lerna版本:
lerna repair
升级
升级后,lerna repair
对于确保应用新版本lerna的任何配置文件更改最为有用:
npm i lerna@latest lerna repair
★5.2.18 lerna watch
用于观察包内的变化,并从存储库的根目录执行命令。
lerna watch -- <command>
有关更多信息,请查看 工作空间监视 功能概述。
值 $LERNA_PACKAGE_NAME
和 $LERNA_FILE_CHANGES
将分别替换为已更改的包和文件。如果在一个周期内检测到多个文件更改,那么 $LERNA_FILE_CHANGES
将列出所有更改,用空格分隔。
使用
$LERNA_PACKAGE_NAME
和$LERNA_FILE_CHANGES
时,需要用反斜杠(\
)对美元符号进行转义。参见下面的例子。
例子:
观察所有包并回显包名和已更改的文件:
lerna watch -- echo \$LERNA_PACKAGE_NAME \$LERNA_FILE_CHANGES
仅监视包 “package-1”, “package-3” 及其依赖项:
lerna watch --scope "package-{1,3}" --include-dependencies -- echo \$LERNA_PACKAGE_NAME \$LERNA_FILE_CHANGES
仅观察包“package-4”及其依赖项,并为发生更改的包运行测试脚本:
lerna watch --scope="package-4" --include-dependencies -- lerna run test --scope=\$LERNA_PACKAGE_NAME
观察所有包,并为已更改的包和所有依赖于它的包运行构建脚本:
lerna watch -- lerna run build --scope=\$LERNA_PACKAGE_NAME --include-dependents
使用 npx
时,如果还提供了用于替换的变量,则必须使用 -c
选项:
使用npx时,如果还提供了用于替换的变量,则必须使用-c选项:
过滤器选项
lerna watch
接受所有过filter-options。过滤器标志可用于选择要监视的特定包。
–verbose 选项
表示在详细模式下运行 lerna watch
,在该模式下,命令在执行前会被记录。
5.3 将 pnpm 与 Lerna 一起使用
Lerna 可以很好地与 pnpm 一起使用,这样可以获得pnpm和Lerna的全部好处。
首先你需要确保全局安装好了这两个模块:
npm i pnpm lerna -g
在 pnpm 工作区中使用时,Lerna 将:
- 使用
pnpm-workspace.yaml
解析包位置; - 在
useWorkspaces: true
中强制执行(在lerna.json
中忽略); - 阻止使用
bootstrap
、link
和add
命令。相反,应直接使用命令来管理依赖项 - 请遵循包依赖项的工作区协议
- 在
lerna version
期间,依赖项将正常更新,但如果workspace:
前缀存在,则会保留前缀。 - 如果使用 工作区别名,则
lerna version
不会碰撞依赖项的版本,因为别名不会指定要碰撞的版本号。
- 图片引用-polyrepo-practice:https://monorepo.tools/images/polyrepo-practice.svg↩︎
- https://monorepo.tools/images/monorepo-polyrepo.svg↩︎↩︎↩︎
- https://monorepo.tools/images/local-computation-caching.svg↩︎
- https://monorepo.tools/images/local-task-orchestration.svg↩︎
- https://monorepo.tools/images/distributed-computation-caching.svg↩︎
- https://monorepo.tools/images/distributed-tasks-execution.svg↩︎
- https://monorepo.tools/images/dependency-graph.svg↩︎
- https://monorepo.tools/images/workspace-analysis.svg↩︎
- https://monorepo.tools/images/consistent-tooling.svg↩︎
- https://monorepo.tools/images/code-generation.svg↩︎
- https://monorepo.tools/images/project-constrains-and-visibility.svg↩︎