We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
在 应用级 Monorepo 优化方案 中有提到过 Yarn duplicate。
使用 yarn 作为包管理器的同学可能会发现:app 在构建时会重复打包某个 package 的不同版本,即使该 package 的这些版本是可以兼容的。
举个 🌰,假设存在以下依赖关系:
当 (p)npm 安装到相同模块时,判断已安装的模块版本是否符合新模块的版本范围,如果符合则跳过,不符合则在当前模块的 node_modules 下安装该模块。即 lib-a 会复用 app 依赖的 [email protected]。
然而,使用 Yarn v1 作为包管理器,lib-a 会单独安装一份 [email protected]。
🤔 思考一下,如果 app 项目依赖的是 lib-b@^1.1.0,这样是不是就没有问题了?
app 安装 lib-b@^1.1.0 时,lib-b 的最新版本是 1.1.0,则 [email protected] 会在 yarn.lock 中被锁定。
yarn.lock
若过了一段时间安装 lib-a,此时 lib-b 的最新版本已经是 1.2.0,那么依旧会出现 Yarn duplicate,所以这个问题还是比较普遍的。
虽然将公司的 Monorepo 项目迁移至了 Rush 以及 pnpm,很多项目依旧还是使用的 Yarn 作为底层包管理工具,并且没有迁移计划。
对于此类项目,我们可以使用 yarn-deduplicate 这个命令行工具修改 yarn.lock 来进行 deduplicate。
按照默认策略直接修改 yarn.lock
npx yarn-deduplicate yarn.lock
--strategy <strategy>
默认策略,会尽量使用已安装的最大版本。
例一,存在以下 yarn.lock:
library@^1.0.0: version "1.0.0" library@^1.1.0: version "1.1.0" library@^1.0.0: version "1.3.0"
修改后结果如下:
library@^1.0.0, library@^1.1.0: version "1.3.0"
library@^1.0.0, library@^1.1.0 会被锁定在 1.3.0(当前安装的最大版本)。
例二:
将 library@^1.1.0 改为 [email protected]
library@^1.0.0: version "1.0.0" [email protected]: version "1.1.0" library@^1.0.0: version "1.3.0"
[email protected]: version "1.1.0" library@^1.0.0: version "1.3.0"
[email protected] 不变,library@^1.0.0 统一至当前安装最大版本 1.3.0。
会尽量使用最少数量的 package,注意是最少数量,不是最低版本,在安装数量一致的情况下,使用最高版本。
例一:
注意:与 highest策略没有区别。
highest
library@^1.0.0, library@^1.1.0: version "1.1.0"
可以发现使用 1.1.0 版本才可以使得安装版本最少。
一把梭很快,但可能带来风险,所以需要支持渐进式的进行改造。
--packages <package1> <package2> <packageN>
指定特定 Package
--scopes <scope1> <scope2> <scopeN>
指定某个 scope 下的 Package
--list
仅输出诊断信息
通过查看 yarn-deduplicate 的 package.json,可以发现该包依赖了以下 package:
源码中主要有两个文件:
cli.js
index.js
可以发现关键点在 getDuplicatedPackages 。
getDuplicatedPackages
首先,明确 getDuplicatedPackages 的实现思路。
假设存在以下 yarn.lock,目标是找出 lodash@^4.17.15 的 bestVersion。
lodash@^4.17.15
bestVersion
lodash@^4.17.15: version "4.17.21" [email protected]: version "4.17.16"
requestedVersion
^4.17.15
installedVersion
4.17.21
requestedVersion(^4.17.15)
4.17.16
fewer
👆🏻 这个过程很重要,是后续代码的指导原则。
const getDuplicatedPackages = ( json: YarnLock, options: Options ): DuplicatedPackages => { // todo }; // 解析 yarn.lock 获取到的 object interface YarnLock { [key: string]: YarnLockVal; } interface YarnLockVal { version: string; // installedVersion resolved: string; integrity: string; dependencies: { [key: string]: string; }; } // 类似于这种结构 const yarnLockInstanceExample = { // ... "lodash@^4.17.15": { version: "4.17.21", resolved: "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c", integrity: "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", dependencies: { "fake-lib-x": "^1.0.0", // lodash 实际上没有 dependencies }, }, // ... }; // 由命令行参数解析而来 interface Options { includeScopes: string[]; // 指定 scope 下的 packages 默认为 [] includePackages: string[]; // 指定要处理的 packages 默认为 [] excludePackages: string[]; // 指定不处理的 packages 默认为 [] useMostCommon: boolean; // 策略为 fewer 时 该值为 true includePrerelease: boolean; // 是否考虑 prerelease 版本的 package 默认为 false } type DuplicatedPackages = PackageInstance[]; interface PackageInstance { name: string; // package name 如 lodash bestVersion: string; // 在当前策略下的最佳版本 requestedVersion: string; // 要求的版本 ^15.6.2 installedVersion: string; // 已安装的版本 15.7.2 }
最终目标是获取 PackageInstance。
PackageInstance
const fs = require("fs"); const lockfile = require("@yarnpkg/lockfile"); const parseYarnLock = (file) => lockfile.parse(file).object; // file 字段通过 commander 从命令行参数获取 const yarnLock = fs.readFileSync(file, "utf8"); const json = parseYarnLock(yarnLock);
我们需要根据指定范围的参数 Options 过滤掉一些 package。
Options
同时 yarn.lock 对象中的 key 都是 lodash@^4.17.15 的形式,这种键名形式不便于查找数据。
key
可以统一以 lodash 为 key,value 为一个数组,数组项为不同的版本信息,方便后续处理,最终我们需要将 yarn.lock 对象转为下面 ExtractedPackages 的结构。
lodash
value
ExtractedPackages
interface ExtractedPackages { [key: string]: ExtractedPackage[]; } interface ExtractedPackage { pkg: YarnLockVal; name: string; requestedVersion: string; installedVersion: string; satisfiedBy: Set<string>; }
satisfiedBy 就是用于存储满足此 package requestedVersion 的所有 installedVersion,默认值为 new Set() ,待后续补充。
satisfiedBy
new Set()
从该 set 中取出满足策略的 installedVersion ,即为 bestVersion。
具体实现如下:
const extractPackages = ( json, includeScopes = [], includePackages = [], excludePackages = [] ) => { const packages = {}; // 匹配 yarn.lock object key 的正则 const re = /^(.*)@([^@]*?)$/; Object.keys(json).forEach((name) => { const pkg = json[name]; const match = name.match(re); let packageName, requestedVersion; if (match) { [, packageName, requestedVersion] = match; } else { // 如果没有匹配数据,说明没有指定具体版本号,则为 * (https://docs.npmjs.com/files/package.json#dependencies) packageName = name; requestedVersion = "*"; } // 根据指定范围的参数过滤掉一些 package // 如果指定了 scopes 数组, 只处理相关 scopes 下的 packages if ( includeScopes.length > 0 && !includeScopes.find((scope) => packageName.startsWith(`${scope}/`)) ) { return; } // 如果指定了 packages, 只处理相关 packages if (includePackages.length > 0 && !includePackages.includes(packageName)) return; if (excludePackages.length > 0 && excludePackages.includes(packageName)) return; packages[packageName] = packages[packageName] || []; packages[packageName].push({ pkg, name: packageName, requestedVersion, installedVersion: pkg.version, satisfiedBy: new Set(), }); }); return packages; };
在完成 packages 的抽离后,我们就有了同一个 package 的不同版本信息。
{ // ... "lodash": [ { "pkg": YarnLockVal, "name": "lodash", "requestedVersion": "^4.17.15", "installedVersion": "4.17.21", "satisfiedBy": new Set() }, { "pkg": YarnLockVal, "name": "lodash", "requestedVersion": "4.17.16", "installedVersion": "4.17.16", "satisfiedBy": new Set() } ] }
我们需要补充其中每一个数组项的 satisfiedBy 字段,并且通过其计算出满足当前 requestedVersion 的 bestVersion,这个过程称之为 computePackageInstances 。
computePackageInstances
相关类型定义如下:
const computePackageInstances = ( packages: ExtractedPackages, name: string, useMostCommon: boolean, includePrerelease = false ): PackageInstance[] => { // todo }; interface PackageInstance { name: string; // package name 如 lodash bestVersion: string; // 在当前策略下的最佳版本 requestedVersion: string; // 要求的版本 ^15.6.2 installedVersion: string; // 已安装的版本 15.7.2 }
实现 computePackageInstances 可以分为三个步骤:
获取全部 installedVersion
/** * versions 记录当前 package 所有 installedVersion 的数据 * satisfies 字段用于存储当前 installedVersion 满足的 requestedVersion * 初始值为 new Set() * 通过该字段的 size 可以分析出满足 requestedVersion 数量最多的 installedVersion * 用于 fewer 策略 */ interface Versions { [key: string]: { pkg: YarnLockVal; satisfies: Set<string> }; } // 当前 package name 对应的依赖信息 const packageInstances = packages[name]; const versions = packageInstances.reduce((versions, packageInstance) => { if (packageInstance.installedVersion in versions) return versions; versions[packageInstance.installedVersion] = { pkg: packageInstance.pkg, satisfies: new Set(), }; return versions; }, {} as Versions);
具体 version 的 satisfies 字段用于存储当前 installedVersion 满足的全部 requestedVersion,初始值为 new Set(),通过该 set 的 size 可以分析出满足 requestedVersion 数量最多的 installedVersion,用于 fewer 策略。
version
satisfies
set
size
补充 satisfiedBy 与 satisfies 字段
// 遍历全部的 installedVersion Object.keys(versions).forEach((version) => { const satisfies = versions[version].satisfies; // 逐个遍历 packageInstance packageInstances.forEach((packageInstance) => { // packageInstance 自身的 installedVersion 必定满足自身的 requestedVersion packageInstance.satisfiedBy.add(packageInstance.installedVersion); if ( semver.satisfies(version, packageInstance.requestedVersion, { includePrerelease, }) ) { satisfies.add(packageInstance); packageInstance.satisfiedBy.add(version); } }); });
根据 satisfiedBy 与 satisfies 计算 bestVersion
packageInstances.forEach((packageInstance) => { const candidateVersions = Array.from(packageInstance.satisfiedBy); // 进行排序 candidateVersions.sort((versionA, versionB) => { // 如果使用 fewer 策略,根据当前 satisfiedBy 中 `satisfies` 字段的 size 排序 if (useMostCommon) { if (versions[versionB].satisfies.size > versions[versionA].satisfies.size) return 1; if (versions[versionB].satisfies.size < versions[versionA].satisfies.size) return -1; } // 如果使用 highest 策略,使用最高版本 return semver.rcompare(versionA, versionB, { includePrerelease }); }); packageInstance.satisfiedBy = candidateVersions; packageInstance.bestVersion = candidateVersions[0]; }); return packageInstances;
这样,我们就找到了同一 package 不同版本的 installedVersion 和所需要的 bestVersion。
const getDuplicatedPackages = ( json, { includeScopes, includePackages, excludePackages, useMostCommon, includePrerelease = false, } ) => { const packages = extractPackages( json, includeScopes, includePackages, excludePackages ); return Object.keys(packages) .reduce( (acc, name) => acc.concat( computePackageInstances( packages, name, useMostCommon, includePrerelease ) ), [] ) .filter( ({ bestVersion, installedVersion }) => bestVersion !== installedVersion ); };
本文通过介绍 Yarn duplicate ,引出 yarn-deduplicate 作为解决方案,并且分析了内部相关实现,期待 Yarn v2 的到来。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
什么是 Yarn duplicate
在 应用级 Monorepo 优化方案 中有提到过 Yarn duplicate。
使用 yarn 作为包管理器的同学可能会发现:app 在构建时会重复打包某个 package 的不同版本,即使该 package 的这些版本是可以兼容的。
举个 🌰,假设存在以下依赖关系:
当 (p)npm 安装到相同模块时,判断已安装的模块版本是否符合新模块的版本范围,如果符合则跳过,不符合则在当前模块的 node_modules 下安装该模块。即 lib-a 会复用 app 依赖的 [email protected]。
然而,使用 Yarn v1 作为包管理器,lib-a 会单独安装一份 [email protected]。
🤔 思考一下,如果 app 项目依赖的是 lib-b@^1.1.0,这样是不是就没有问题了?
app 安装 lib-b@^1.1.0 时,lib-b 的最新版本是 1.1.0,则 [email protected] 会在
yarn.lock
中被锁定。若过了一段时间安装 lib-a,此时 lib-b 的最新版本已经是 1.2.0,那么依旧会出现 Yarn duplicate,所以这个问题还是比较普遍的。
虽然将公司的 Monorepo 项目迁移至了 Rush 以及 pnpm,很多项目依旧还是使用的 Yarn 作为底层包管理工具,并且没有迁移计划。
对于此类项目,我们可以使用 yarn-deduplicate 这个命令行工具修改
yarn.lock
来进行 deduplicate。yarn-deduplicate — The Hero We Need
基本使用
按照默认策略直接修改
yarn.lock
处理策略
--strategy <strategy>
highest 策略
默认策略,会尽量使用已安装的最大版本。
例一,存在以下
yarn.lock
:修改后结果如下:
library@^1.0.0, library@^1.1.0 会被锁定在 1.3.0(当前安装的最大版本)。
例二:
将 library@^1.1.0 改为 [email protected]
修改后结果如下:
[email protected] 不变,library@^1.0.0 统一至当前安装最大版本 1.3.0。
fewer 策略
会尽量使用最少数量的 package,注意是最少数量,不是最低版本,在安装数量一致的情况下,使用最高版本。
例一:
修改后结果如下:
注意:与
highest
策略没有区别。例二:
将 library@^1.1.0 改为 [email protected]
修改后结果如下:
可以发现使用 1.1.0 版本才可以使得安装版本最少。
渐进式更改
一把梭很快,但可能带来风险,所以需要支持渐进式的进行改造。
--packages <package1> <package2> <packageN>
指定特定 Package
--scopes <scope1> <scope2> <scopeN>
指定某个 scope 下的 Package
诊断信息
--list
仅输出诊断信息
yarn-deduplicate 原理解析
基本流程
通过查看 yarn-deduplicate 的 package.json,可以发现该包依赖了以下 package:
源码中主要有两个文件:
cli.js
,命令行相关能力。解析参数并根据参数执行index.js
中的方法。index.js
。主要逻辑代码。可以发现关键点在
getDuplicatedPackages
。Get Duplicated Packages
首先,明确
getDuplicatedPackages
的实现思路。假设存在以下
yarn.lock
,目标是找出lodash@^4.17.15
的bestVersion
。yarn.lock
分析出lodash@^4.17.15
的requestedVersion
为^4.17.15
,installedVersion
为4.17.21
;requestedVersion(^4.17.15)
的所有installedVersion
,即4.17.21
与4.17.16
;installedVersion
中挑选出满足当前策略的bestVersion
(若当前策略为fewer
,那么lodash@^4.17.15
的bestVersion
为4.17.16
,否则为4.17.21
)。👆🏻 这个过程很重要,是后续代码的指导原则。
类型定义
最终目标是获取
PackageInstance
。获取
yarn.lock
数据yarn.lock 对象结构美化
我们需要根据指定范围的参数
Options
过滤掉一些 package。同时
yarn.lock
对象中的key
都是lodash@^4.17.15
的形式,这种键名形式不便于查找数据。可以统一以
lodash
为key
,value
为一个数组,数组项为不同的版本信息,方便后续处理,最终我们需要将yarn.lock
对象转为下面ExtractedPackages
的结构。satisfiedBy
就是用于存储满足此 packagerequestedVersion
的所有installedVersion
,默认值为new Set()
,待后续补充。具体实现如下:
在完成 packages 的抽离后,我们就有了同一个 package 的不同版本信息。
我们需要补充其中每一个数组项的
satisfiedBy
字段,并且通过其计算出满足当前requestedVersion
的bestVersion
,这个过程称之为computePackageInstances
。Compute Package Instances
相关类型定义如下:
实现
computePackageInstances
可以分为三个步骤:installedVersion
;satisfiedBy
字段;satisfiedBy
计算出bestVersion
。获取全部
installedVersion
具体
version
的satisfies
字段用于存储当前installedVersion
满足的全部requestedVersion
,初始值为new Set()
,通过该set
的size
可以分析出满足requestedVersion
数量最多的installedVersion
,用于fewer
策略。补充
satisfiedBy
与satisfies
字段根据
satisfiedBy
与satisfies
计算bestVersion
这样,我们就找到了同一 package 不同版本的
installedVersion
和所需要的bestVersion
。完成 getDuplicatedPackages
结语
本文通过介绍 Yarn duplicate ,引出 yarn-deduplicate 作为解决方案,并且分析了内部相关实现,期待 Yarn v2 的到来。
The text was updated successfully, but these errors were encountered: