terraform-cdk 依赖版本固化工具 dependency-pinner:将 yarn 工作区依赖锁定到 yarn.lock 实际版本

  • IaC
  • 云原生
  • 开发工具

【免费下载链接】terraform-cdk

Define infrastructure resources using programming constructs and provision them using HashiCorp Terraform

项目地址: https://gitcode.com/gh_mirrors/te/terraform-cdk
点击查看 免费下载

本篇介绍 terraform-cdk 仓库 tools/ 目录下的 dependency-pinner 一次性工具。它解决的问题是:yarn 生态没有现成工具能像 npm 的 package-lock.json 场景那样,把各 package.json 中宽松的版本范围统一替换为 lockfile 里实际锁定的版本。读完本文,你将理解该工具的适用范围、运行方式,以及从源码视角看清它如何解析 yarn.lock、识别工作区内部包并批量改写依赖声明。

一、工具定位:为什么需要自己写一个

tools/dependency-pinner/README.md 对工具的定位非常明确:

这是一个一次性(one-off)工具,用于把本项目中所有依赖锁定(pin)到 yarn.lock 中记录的实际版本。目前市面上没有针对 yarn 实现这一点的工具(npm 的 package-lock.json 场景有),所以团队自己写了一个。由于不希望在本项目之外维护或发布它,就作为参考代码保留在这里。

理解这句话需要放在 terraform-cdk 的仓库结构背景下看:

  • 根 package.json 使用 yarn workspaces 组织整个 monorepo,工作区包含 packages/*、packages/@cdktf/*、examples/*(含 python/typescript/java/csharp/go 各语言的示例目录)、tools/generate-function-bindings 等;
  • lerna.json 配置了 "npmClient": "yarn" 与 "version": "0.0.0",即开发阶段所有包统一使用 0.0.0 占位版本,真实版本号由发布流程在 CI 中通过 lerna 注入;
  • 根 package.json 还声明了 "packageManager": "yarn@1.18.0",锁文件为 v1 格式的 yarn.lock。

在这种结构下,各包的 dependencies/devDependencies 中如果写着 ^x.y.z 这类范围声明,实际安装结果完全由 yarn.lock 决定。dependency-pinner 的作用就是把这个"隐式锁定"变成"显式锁定":把 package.json 中的版本范围直接改写成 yarn.lock 中该依赖被解析到的确切版本号,使得依赖声明与锁文件完全一致,避免不同环境下重复解析产生漂移。

二、运行方式

README 给出的操作步骤只有两步,且都依赖仓库根目录的依赖安装结果:

  1. 在仓库根目录执行 yarn 安装依赖(这样根工作区把 ts-node 等运行所需依赖准备好);
  2. 进入 tools/dependency-pinner/ 目录执行 yarn start。

其中 yarn start 具体执行什么,可以从 tools/dependency-pinner/package.json 中看到:

{
    "name": "@tools/dependency-pinner",
    "version": "0.0.0",
    "scripts": {
        "start": "ts-node ./pin-dependencies.ts"
    },
    "devDependencies": {
        "@types/yarnpkg__lockfile": "^1.1.9",
        "ts-node": "^10.9.1"
    },
    "dependencies": {
        "@yarnpkg/lockfile": "^1.1.0",
        "execa": "^5.1.1",
        "glob": "^8.0.3"
    }
}

即通过 ts-node 直接以 TypeScript 运行 pin-dependencies.ts,无需预编译。核心依赖只有两个:@yarnpkg/lockfile(官方 yarn v1 锁文件解析库)与 glob(文件匹配)。

三、源码逐段解析:pin-dependencies.ts 的完整逻辑

整个实现只有 70 行,下面按代码顺序拆解(对应 pin-dependencies.ts)。

1. 定位仓库根目录

const PROJECT_ROOT = path.join(__dirname, "..", "..");

__dirname 是 tools/dependency-pinner/,向上两级即仓库根目录。因此工具必须放在仓库内这个固定位置才能正确工作,这也解释了 README 中"先根目录 yarn、再本目录 yarn start"的运行前提。

2. 排除工作区内部包

const ignoredDependencies = [
  "@cdktf/cli-core",
  "@cdktf/commons",
  "@cdktf/hcl2cdk",
  "@cdktf/hcl2json",
  "@cdktf/provider-generator",
  "@cdktf/provider-schema",
  "cdktf",
  "cdktf-cli",
];

这 8 个名字正是本仓库自身的发布包(packages/cdktf、packages/cdktf-cli 与 packages/@cdktf/* 下的 6 个子包)。它们在各示例和包的 package.json 中以 "cdktf": "0.0.0" 这样的占位版本存在(例如 examples/typescript/documentation/package.json 中的 "cdktf": "0.0.0"),由 yarn workspaces 本地链接提供,而不是从 registry 安装。它们不会出现在 yarn.lock 的可解析条目中,因此必须整体跳过,否则会产生无意义的告警或错误改写。

3. 解析 yarn.lock

const file = fs.readFileSync(path.join(PROJECT_ROOT, "yarn.lock"), "utf8");
const { type, object: yarnLockFile } = lockfile.parse(file);
if (type !== "success") {
  throw new Error("Failed to parse yarn.lock");
}

工具读取根目录的 yarn.lock,用 @yarnpkg/lockfile 解析为键值对象 yarnLockFile。yarn v1 锁文件的键格式是 "依赖名@版本范围"(同一范围被多处引用时多个键以逗号合并),值对象带有 version、resolved、integrity 字段。例如 yarn.lock 中的:

constructs@10.4.2:
  version "10.4.2"
  resolved "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz#..."
  integrity sha512-...

解析失败时直接抛错终止,保证"锁文件是唯一事实来源"这一前提不成立时不会继续写坏任何 package.json。

4. 圈定待处理的 package.json

const packageJsons = glob.sync(
  "{packages/*/package.json,packages/@cdktf/*/package.json,examples/*/*/package.json}",
  { cwd: PROJECT_ROOT },
);

glob 模式覆盖了三个位置:

  • packages/*/package.json:顶层包 cdktf、cdktf-cli;
  • packages/@cdktf/*/package.json:@cdktf scope 下的 6 个包;
  • examples/*/*/package.json:各语言示例(examples/typescript/*、examples/python/* 等)。

值得注意的是该模式不包含 tools/、test/ 与根 package.json,即工具只固化产品包与示例的依赖声明。

5. 逐条依赖查找并改写

Object.entries(pkg.dependencies || {}).forEach(([dep, version]) => {
  if (ignoredDependencies.includes(dep)) {
    return;
  }
  if (yarnLockFile[`${dep}@${version}`]) {
    pkg.dependencies[dep] = yarnLockFile[`${dep}@${version}`].version;
  } else {
    console.info(`Could not find ${dep}@${version} in yarn.lock`);
  }
});

devDependencies 的处理逻辑与之完全相同(L54-L64)。核心是这一行:

pkg.dependencies[dep] = yarnLockFile[`${dep}@${version}`].version;

即用 依赖名@package.json 中当前声明的范围 作为键去锁文件对象里精确查找,命中后把声明改写为值对象中的确切 version。这里有一个隐含行为:查找是按"依赖名 + 原始范围字符串"整键匹配的。如果 package.json 里的范围写法与锁文件中的键不完全一致(例如范围被更新过但锁文件尚未重新生成,或使用了别名 pkg@npm:other 这类 yarn v1 别名语法导致键形式变化),该条目不会被改写,只会打印一条 Could not find xxx@yyy in yarn.lock 的提示,工具不会抛错、也不会中断其余条目的处理——属于"尽力而为"的语义。

6. 写回文件

fs.writeFileSync(
  path.join(PROJECT_ROOT, packageJsonPath),
  JSON.stringify(pkg, null, 2),
  "utf8",
);

无论该文件是否发生实际变更,最后都以 2 空格缩进整体重写。因此运行后所有被圈定的 package.json 会统一为 JSON.stringify 的标准格式。

四、仓库中的运行效果佐证

从当前仓库各包 package.json 的实际内容,可以看到 pinning 之后依赖声明的典型形态——外部依赖大多被写成不带范围前缀的精确版本:

  • examples/typescript/documentation/package.json 中:"constructs": "10.4.2"、"@types/jest": "29.5.14"、"@types/node": "20.17.51"、"tsx": "4.6.1"、"typescript": "5.4.5",均为精确版本,与 yarn.lock 中对应条目的 version 一致;
  • packages/cdktf/package.json 的 devDependencies 中同样可见 "constructs": "10.4.2"、"@types/jest": "29.5.14"、"@types/node": "18.19.101" 等精确声明,与同文件中仍带 ^ 前缀的声明(如 "@eslint/eslintrc": "^3.3.1")并存——可以推断部分依赖是在引入该工具之前就已手动锁定的,而新加入的依赖则通过工具批量固化;
  • 对照 yarn.lock 中 constructs@10.4.2 条目(version "10.4.2"),正好对应工具 yarnLockFile[\${dep}@${version}`].version` 的查找结果。

这也印证了 README 所说的目标状态:package.json 声明的版本与 yarn.lock 锁定的版本一一对应,任何一处漂移都会表现为 Could not find ... 提示而非静默改写。

五、与仓库其他发布工具的关系

dependency-pinner 不是孤立的,它和仓库发布链路中另外两个机制共同保证了"声明—锁定—发布"三者一致:

  1. 0.0.0 占位 + lerna 注入真实版本。开发期所有工作区包版本为 0.0.0(见 lerna.json),而 CI 构建时通过 tools/align-version.sh 执行 npx lerna version <version> --exact --force-publish=*,把根 package.json 中的版本号以精确模式注入所有包,"so artifacts include the actual version instead of 0.0.0"。--exact 参数保证了写入产物的是精确版本而非范围声明,与 pinner 的思路一脉相承。
  2. 贡献历史中反复出现的 pinning 话题。CONTRIBUTING.md 在讲解 backport 流程时,示例分支名即为 backport-pr-1767-stop-pinning-jest,说明"是否对某依赖做版本固化"在该仓库的维护历史中是一个反复权衡的话题,dependency-pinner 正是这类权衡的工程化出口。

六、边界与注意事项

结合 README 与 pin-dependencies.ts 源码,使用或借鉴该工具时需注意以下边界:

  • 一次性参考工具,不对外发布:README 明确它只是本项目内的参考实现,不承担维护与版本承诺;
  • 只处理 dependencies 与 devDependencies:peerDependencies、optionalDependencies、resolutions(根 package.json 中存在 "**/@types/react": "18.3.18" 这类条目)均不在改写范围内;
  • 查找为整键精确匹配:键必须严格等于 依赖名@声明范围,范围写法与锁文件键不一致时只告警不修改,需要人工核对;
  • 原地重写文件:对每个命中的 package.json 都会以 JSON.stringify(pkg, null, 2) 覆盖写回,文件键顺序会保持原 JS 对象顺序,但整体格式统一为 2 空格缩进;
  • 不修改 yarn.lock、不触发安装:工具只改声明文件,锁文件仍是安装事实来源;改写后如需验证,应另行执行 yarn 确认无新增解析;
  • 依赖工具位置:PROJECT_ROOT 由 __dirname 向上两级推算,脚本必须保持在 tools/dependency-pinner/ 下运行,否则路径推算失效。

七、小结

terraform-cdk 的 dependency-pinner 用一个 70 行的 TypeScript 脚本填补了 yarn v1 工作区的依赖固化空白:以 yarn.lock 为唯一事实来源,跳过 8 个仓库内部发布包,将 packages/ 与 examples/ 下所有 package.json 中的版本声明改写为锁定的精确版本。它的源码完整、行为透明、失败语义清晰(解析失败即抛错、条目缺失即告警),既可以直接在本仓库按 README 两步运行,也可以作为其他 yarn v1 monorepo 实现同类固化工具的参考实现。

  • IaC
  • 云原生
  • 开发工具

【免费下载链接】terraform-cdk

Define infrastructure resources using programming constructs and provision them using HashiCorp Terraform

项目地址: https://gitcode.com/gh_mirrors/te/terraform-cdk
点击查看 免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值