什么是 Yarn duplicate
应用 yarn 作为包管理器的同学可能会发现:常常会呈现 package 在构建时会被反复打包,即便这些 package 的版本是能够相互兼容的。
举个 🌰,假如存在以下依赖关系:
当 (p)npm 装置到雷同模块时,判断已装置的模块版本是否合乎新模块的版本范畴,如果合乎则跳过,不合乎则在以后模块的 node_modules 下装置该模块。即 lib-a 会复用 app 依赖的 lib-b@1.1.0。
然而,应用 Yarn v1 作为包管理器,lib-a 会独自装置一份 lib-b@1.2.0。
- difference between npm and yarn behavior with nested dependencies #3951
- Yarn installing multiple versions of the same package
- Yarn v2 supports package deduplication natively
🤔 思考一下,如果 app 我的项目依赖的是 lib-b@^1.1.0,这样是不是就没有问题了?
app 装置 lib-b@^1.1.0 时,lib-b 的最新版本是 1.1.0,则 lib-b@1.1.0 会在 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
npx yarn-deduplicate yarn.lock
解决策略
--strategy <strategy>
highest 策略
默认策略,会尽量应用已装置的最大版本。
例一,存在以下 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 改为 library@1.1.0
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.1.0:
version "1.1.0"
library@^1.0.0:
version "1.3.0"
library@1.1.0 不变,library@^1.0.0 对立至以后装置最大版本 1.3.0。
fewer 策略
会尽量应用起码数量的 package, 留神是起码数量,不是最低版本,在装置数量统一的状况下,应用最高版本 。
例一:
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"
留神:与 highest
策略没有区别 。
例二:
将 library@^1.1.0 改为 library@1.1.0
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.1.0"
能够发现应用 1.1.0 版本才能够使得装置版本起码。
渐进式更改
一把梭很快,但可能带来危险,所以须要反对渐进式的进行革新。
--packages <package1> <package2> <packageN>
指定特定 Package
--scopes <scope1> <scope2> <scopeN>
指定某个 scope 下的 Package
诊断信息
--list
仅输入诊断信息
yarn-deduplicate 原理解析
根本流程
通过查看 yarn-deduplicate 的 package.json,能够发现该包依赖了以下 package:
- commander 残缺的 node.js 命令行解决方案;
- @yarnpkg/lockfile 解析或写入 yarn.lock 文件;
- semver The semantic versioner for npm,能够用来判断装置版本是否满足 package.json 要求版本。
源码中次要有两个文件:
cli.js
,命令行相干能力。解析参数并依据参数执行index.js
中的办法。index.js
。次要逻辑代码。
能够发现关键点在 getDuplicatedPackages
。
Get Duplicated Packages
首先,明确 getDuplicatedPackages
的实现思路。
假如存在以下 yarn.lock
,指标是找出 lodash@^4.17.15
的 bestVersion
。
lodash@^4.17.15:
version "4.17.21"
lodash@4.17.16:
version "4.17.16"
- 通过
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
)。
类型定义
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
。
获取 yarn.lock
数据
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);
Extract Packages
咱们须要依据指定范畴的参数过滤掉一些 package。
同时 yarn.lock
对象中的 key 都是 lodash@^4.17.15
的模式,可能还存在以 lodash@4.17.16
为 key 的 value,这种键名模式不便于查找数据。
咱们能够对立以 lodash
包名为 key,value 为一个数组,数组项为不同的版本信息,不便后续解决。
interface ExtractedPackage {[key: string]: {
pkg: YarnLockVal;
name: string;
requestedVersion: string;
installedVersion: string;
satisfiedBy: Set<string>;
};
}
interface ExtractedPackages {[key: string]: ExtractedPackage[];}
satisfiedBy
就是用于存储满足此 package requestedVersion
的所有 installedVersion
,默认值为 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 的抽离后,咱们须要补充其中的 satisfiedBy
字段,并且通过其计算出 bestVersion
,即实现 computePackageInstances
。
Compute Package Instances
相干类型定义如下:
interface PackageInstance {
name: string; // package name 如 lodash
bestVersion: string; // 在以后策略下的最佳版本
requestedVersion: string; // 要求的版本 ^15.6.2
installedVersion: string; // 已装置的版本 15.7.2
}
const computePackageInstances = (
packages: ExtractedPackages,
name: string,
useMostCommon: boolean,
includePrerelease = false
): PackageInstance[] => {// todo};
实现 computePackageInstances
能够分为三个步骤:
- 获取以后 package 的全副
installedVersion
信息; - 补充
satisfiedBy
字段; - 通过
satisfiedBy
计算出bestVersion
。
获取 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);
补充 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;
实现 getDuplicatedPackages
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 的到来。