monorepo 的 Rush.js 方案


前言

记录一下之前使用 Rush 的一些知识点,我记得 2022 年的时候, rush.js 的方案还是比较有价值的,但是在现在看来, turborepo 和 nx 的方案更加有价值。

Rush 是微软 Rush Stack 中的一个工具。

场景

我有一个 web 在线文档项目, 里面有很多的模块,他们之间有复杂的依赖关系,所以我通过 rush.js 来管理这些模块。

优点

  • 文档有中文
  • 构建有缓存,可以加快构建速度
  • 自身管理了包管理工具的版本,项目环境容易搭建
  • 解决包之间的幽灵依赖,所有依赖都更加清晰, 依赖不才有隐式声明
  • 确保了多个模块,依赖的同一个库的版本的一致性
  • 自带了 hooks 方案,不需要 husky
  • 自定义能力强, 可扩展支持自定义命令

特殊之处 (可能是缺点)

  • 根目录下没有 package.jsonnode_modules 都在 common/temp/node_modules 下,然后子包下有 node_modules 会链接到这儿目录中,这是为了保证 Rush 的幻影依赖能正常工作
    • Rush 如何解决问题的: rush install 指令可以扫描所有潜在的父目录并在发现 node_modules 中存在幻影依赖时发出警告。
  • 所有命令都是 rush, rushx 封装的
  • 没有项目维度的包管理, 需要子包自己安装, 比如 typescript, eslint
  • 和 VSC 结合,需要配置一些工具识别, 比如 eslint, prettier 插件, 他们是如何找本地版本的?开发状态下,都是打开子包开发的, 子包内有对于的 node_moudles 可查找?

使用示例

安装

  • npm install -g @microsoft/rush
  • 可在 $ZSH_CUSTOM 目录中,创建 rush-completion.zsh, 添加 completion 功能: Configuring tab completion | Rush
  • 最好远程仓库也设置好,方便对比版本。

初始化

文件约定

配置文件参考 | Rush

  • /common/: 放置配置文件, rush 自带的
  • /apps/: 放置应用类型项目, 比如 landing Page, 文档站点.
  • /packages/: 放置需要发布的包.
  • /libs/ 或者 /features/: 放置内部共享的代码, 可能不需要编译, 也不需要发布. 比如共享的类型定义
  • /tools/ or /scripts/: 放置一些脚本文件, contains scripts that act on your code base. This could be database scripts, custom executors, or workspace generators.

一些子命令

  • rush install 进入项目后,安装全部依赖
  • rush add: 安装或者更新依赖, 使用 rush add 而不是 pnpm add 添加项目依赖. 添加后会自动执行 rush update 更新整体项目依赖.
  • rush update: 增量更新
  • rush rebuild: 强制重新构建
  • rush check: 检查多个项目是否依赖同一个库的不同版本,在 monorepo 环境下,这种行为是不可取的。
  • rushx start: 进入子项目目录后运行,用户开发项目

rushx 指令与 npm run 的指令类似,但它输入更简单、报错提示更友好、同时还有命令行帮助信息。rush 命令,可以仓库中任意子目录中执行。

实操

当新项目建立后,我们需要:

  • 管理包之间的关系:添加到 rush.json 配置中
  • 为包添加依赖
  • 添加项目共享的依赖, 比如开发时候,使用的 prettier.安装为共享开发依赖项, 需要手动修改 common/config/rush/common-versions.json 文件,然后执行 rush update, 后续可以使用rushx 调用

创建需要管理的项目

手动配置 rush.json, 添加 projects, rush 才能感知要管理的 package.然后再执行 rush update, 会更新 common 下的一些文件。

配置一些 config

Recommended settings | Rush

比如建议开启 ensureConsistentVersionsstrictPeerDependenciesallowMostlyStandardPackageNames 等。

配置 git hooks

Installing Git hooks | Rush

hooks 文件放在 common/git-hooks/ 下, 通过 rush install 时候, 自动拷贝到 .git/hooks/ 下。可以在 hook 中通过 install-run-rush 脚本调用配置在 commoand-line 下的命令。比如在 common/git-hooks/pre-commit 中:

#!/bin/sh
# Called by "git commit" with no arguments.  The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.

# Invoke the "rush prettier" custom command to reformat files whenever they
# are committed. The command is defined in common/config/rush/command-line.json
# and uses the "rush-prettier" autoinstaller.
node common/scripts/install-run-rush.js prettier || exit $?

配置自定义命令

Custom commands | Rush

扩展 rush xx 命令。command kind 有两种类型:

    • global: 全局配置调用 shell 或者 autoinstaller 模块
  • bulk: Rush 的 “bulk” 类型指令将会在每个项目中都被调用,Rush 会寻找项目 package.json 内的 “scripts” 字段中匹配该命令行的字段。

pnpm 结合的问题

hoist 无法生效,只能提升到 common/temp/node_modules 下, cli 一般可以搞定

VSC 的结合问题

由于没有 root 的 package.json 和 node_moldules, 大概率会影响 eslint 等插件找对于命令的路径。可能找不到, rushstack/eslint/eslint-config at main · microsoft/rushstack

  • 配置 workspace
  • 能单独打开子目录,调用 rushx 开发
  • 子目录下,还是有 node_modules 的, 所以通过 monorepo 单独打开某个模块,是能正常工作的
  • root 目录下, 不建议配置 prettier 这种全局的配置, 因为这一层没有 node_modules,

配置 workspace

eslint 的问题

vscode 加载 .eslint 配置文件的时候, 可能无法找到对于的 node_modules, 从而失败。

// This is a workaround for https://github.com/eslint/eslint/issues/3458
require('@rushstack/eslint-config/patch/modern-module-resolution');

prettier 的问题

默认 prettier 也是会使用 node_module 的。 这是它 resolution 逻辑: Prettier - Code formatter - Visual Studio Marketplace, Version 10.0.0 of this extension will include prettier 3.0.0 after it is out of preview.

CI 构建

执行 rush install 或者 rush update 的时候,会生成 common/scripts 相关文件, 文件在这里 rushstack/libraries/rush-lib/src/scripts at main · microsoft/rushstack

新项目, 需要先执行 rush update, 不然直接运行 rush install 报错:

ERROR: The standard files in the "common/scripts" folders need to be updated for this Rush version.  Please run "rush update" and commit the changes.

启用 CI | Rush, 项目初始化, 就会带有 common/scripts/xxx.js 脚本。这些脚本的作用:

install-run-rush.js

  • 寻找 rush.json 文件
  • 读取指定在该文件内的 rushVersion
  • 自动在 common/temp/install-run 目录下安装该版本的 Rush
  • 使用仓库内的 .npmrc 文件进行适当的设置
  • 之后调用 Rush 工具链,并传递给它任何你提供的命令行参数

install-run-rushx.jsinstall-run-pnpm.js

都是封装了 install-run-rush 的调用。

install-run.js

执行任意的 NPM 包。比如: node common/scripts/install-run.js qrcode@1.2.2 qrcode https://rushjs.io,

它必须要包含包名和其版本(可以是语义化版本,但最好写确定版本),它还需要第二个参数来指令可执行文件的具体名称

一些 Tips

没有 root 的 package.json 的问题

对于大多数 monorepo 的项目结构, 都会有 root 层级的 package.json, 里面会安装上对整个项目全局而言的共享依赖, 比如 typescript, eslint。如果 rush.js 没有这个 package.json 以及对于的 node_modules 那么 IDE 的插件识别上, 可能会有问题。并且如何使用 husky 这种默认要求 root 有 package 的包, 就会冲突。

但对于子包而言,通常他们自身也是独立可运行的项目,顶层的 ”共享“ 依赖, 他们自己也有依赖声明和安装。

解决方案:

  • 寻找 rush.js 内置的替代方案, 比如 git hook
  • 包通过全局安装,比如 prettier
  • 创建自动安装程序, Autoinstallers | Rush,这相当于一个自定义的 npm 包, 配置我们自定义一个 commonad 调用:启用 Prettier | Rush
  • install-run.js 去执行

如何配置 eslint/prettier/husky 等工具

参考