Monorepo 的這些坑,我們幫你踩過了!
前言
筆者目前所在團隊是使用 Monorepo 的方式管理所有的業(yè)務(wù)項目,而隨著項目的增多,穩(wěn)定性以及開發(fā)體驗受到挑戰(zhàn),諸多問題開始暴露,可以明顯感受到現(xiàn)有的 Monorepo 架構(gòu)已經(jīng)不足以支撐日漸龐大的業(yè)務(wù)項目。
現(xiàn)有的 Monorepo 是基于 yarn workspace 實現(xiàn),通過 link 倉庫中的各個 package,達(dá)到跨項目復(fù)用的目的。package manager 也理所當(dāng)然的選擇了 yarn,雖然依賴了 Lerna,由于發(fā)包場景較為稀少,基本沒有怎么使用。
可以總結(jié)為以下三點:
通過 yarn workspace link 倉庫中的 package 使用 yarn 作為 package manager 管理項目中的依賴 通過 lerna 在應(yīng)用 app 構(gòu)建前按照依賴關(guān)系構(gòu)建其依賴的 packages
存在的問題
命令不統(tǒng)一
存在三種命令
yarn yarn workspace lerna
新人上手容易造成誤解,部分命令之間功能存在重疊。
發(fā)布速度慢

如果我們需要發(fā)布 app1,則會
全量安裝依賴,app1、app2、app3 以及 package1 至 package6 的依賴都會被安裝; package 全部被構(gòu)建,而非僅有 app1 依賴的 package1 與 package2 被構(gòu)建。
Phantom dependencies
一個庫使用了不屬于其 dependencies 里的 Package 稱之為 Phantom dependencies(幻影依賴、幽靈依賴、隱式依賴),在現(xiàn)有 Monorepo 架構(gòu)中該問題被放大(依賴提升)。

由于無法保證幻影依賴的版本正確性,給程序運行帶來了不可控的風(fēng)險。app 依賴了 lib-a,lib-a 依賴了 lib-x,由于依賴提升,我們可以在 app 中直接引用 lib-x,這并不可靠,我們能否引用到 lib-x,以及引用到什么版本的 lib-x 完全取決于 lib-a 的開發(fā)者。
NPM doppelgnger
相同版本的 Package 可能安裝多份,打包多份。
假設(shè)存在以下依賴關(guān)系

最終依賴安裝可能存在兩種結(jié)果:
lib-x@^1 * 1 份,lib-x@^2 * 2 份 lib-x@^2 * 1 份,lib-x@^1 * 2 份
最終本地會安裝 3 份 lib-x,打包時也會存在三份實例,如果 lib-x 要求單例,則可能會造成問題。
Yarn duplicate
Yarn duplicate 及解決方案[1]
假設(shè)存在以下依賴關(guān)系

當(dāng) (p)npm 安裝到相同模塊時,判斷已安裝的模塊版本是否符合新模塊的版本范圍,如果符合則跳過,不符合則在當(dāng)前模塊的 node_modules 下安裝該模塊。即 lib-a 會復(fù)用 app 依賴的 [email protected]。
然而,使用 Yarn v1 作為包管理器,lib-a 會單獨安裝一份 [email protected]。
difference between npm and yarn behavior with nested dependencies #3951[2] Yarn installing multiple versions of the same package[3] yarn-deduplicate-Cleans up yarn.lock by removing duplicates.[4] Yarn v2 supports package deduplication natively[5]
peerDependencies 風(fēng)險
Yarn 依賴提升,在 peerDependencies 場景下可能導(dǎo)致 BUG。
app1 依賴 [email protected] app2 依賴 [email protected] [email protected] 將 [email protected] 作為 peerDependency,故 app2 也應(yīng)該安裝 [email protected]
若 app2 忘記安裝 [email protected],那么結(jié)構(gòu)如下
--apps
--app1
--app2
--node_modules
[email protected]
[email protected]
此時 [email protected] 會錯誤引用 [email protected]。
Package 引用規(guī)范缺失
目前項目內(nèi)存在三種引用方式:
源碼引用:使用包名引用。需要配置宿主項目的構(gòu)建腳本,將該 Package 納入構(gòu)建流程。類似于直接發(fā)布一個 TypeScript 源碼包,引用該包的項目需要做一定的適配。 源碼引用:使用文件路徑引用??梢岳斫狻八拗髟谧陨?src 之外的源文件”,即宿主項目源代碼的一部分,而非 Package。宿主需要提供該所有依賴,在 Yarn 依賴提升的前提下達(dá)到了跨項目復(fù)用,但存在較大風(fēng)險。 產(chǎn)物引用。打包完成,直接通過包名引用產(chǎn)物。
Package 引用版本不確定性
假設(shè)一個 Monorepo 中的 package1 發(fā)布至了 npm 倉庫,那么 Monorepo 中的 app1 應(yīng)當(dāng)如何在 package.json 中編寫引用 package1 的版本號?
package1/packag.json
{
"name": "package1",
"version": "1.0.0"
}
app1/package.json
{
"name": "app1",
"dependencies": {
"package-1": "?" // 這里的版本號應(yīng)該怎么寫?`*` or `1.0.0`
}
}
在處理 Monorepo 中項目的互相引用時,Yarn 會進行以下幾步判斷:
判斷當(dāng)前 Monorepo 中,是否存在匹配 app1 所需版本的 package1; 若存在,執(zhí)行 link 操作,app1 直接使用本地 package1; 若不存在,從遠(yuǎn)端 npm 倉庫拉取符合版本的 package1 供 app1 使用。
需要特別注意的是:
*無法匹配prerelease 版本?? Workspace package with prerelease version and wildcard dep version #6719[6]。
假設(shè)存在以下場景:
package1 此前已經(jīng)發(fā)布了 1.0.0版本,此時遠(yuǎn)端倉庫與本地 Monorepo 中代碼一致;產(chǎn)品同學(xué)提了一個只服務(wù)于 Monorepo 內(nèi)部應(yīng)用的需求; package1 在 1.0.0版本下迭代,無需變更版本號發(fā)布;Yarn 判斷 Monorepo 中的 package1 版本滿足了 app1 所需版本( *或1.0.0);app1 順利使用上 package1 的最新特性。
直到某天,該需求特性需要提供給外部業(yè)務(wù)方使用。
pacakge1 將版本改為 1.0.0-beta.0并進行發(fā)版;Yarn 判斷當(dāng)前 Monorepo 中的 package1 版本不滿足 app1 所需版本; 從遠(yuǎn)端拉取 [email protected]供 app1 使用;遠(yuǎn)端 [email protected]已經(jīng)落后 app1 先前使用的本地[email protected]太多;準(zhǔn)備事故通報以及復(fù)盤。
這種不確定性,導(dǎo)致引用此類 package 時會經(jīng)常犯嘀咕:我到底引用的是本地版本還是遠(yuǎn)端版本?為什么有時候是本地版本,有時候是遠(yuǎn)端版本?我想用上 package1 的最新內(nèi)容還需要時刻保持與 package1 的版本號保持一致 ,那我干嘛用 Monorepo ?
yarn.lock 沖突
(p)npm 支持自動化解決 lockfile 沖突,yarn 需要手動處理,在大型 Monorepo 場景下,幾乎每次分支合并都會遇到 yarn.lock 沖突。
不解決沖突無腦 yarn,yarn.lock會直接失效,全部版本更新到package.json最新,風(fēng)險太大,失去lockfile的意義;人工解決沖突往往會出現(xiàn) Git conflict with binary files,只能使用master的提交再重新yarn,流程繁瑣。
Automatically resolve conflicts in lockfile · Issue #2036 · pnpm/pnpm[7]
可以發(fā)現(xiàn),現(xiàn)有 Monorepo 管理方式缺陷過多,隨著其內(nèi)項目的不斷增加,構(gòu)建速度會越來越慢,同時程序的健壯性無法得到保證。僅憑開發(fā)人員自覺是不可靠的,我們需要一套解決方案。
推薦閱讀:node_modules 困境[8]
解決方案
pnpm[9]
Fast, disk space efficient package packageManager
在 npm@3 之前, node_modules 的結(jié)構(gòu)是干凈且可預(yù)測的,因為 node_modules 中的每個依賴項都有其自己的 node_modules 文件夾,其所有依賴項都在 package.json 中指定。
node_modules
└─ foo
├─ index.js
├─ package.json
└─ node_modules
└─ bar
├─ index.js
└─ package.json
但是這樣帶來了兩個很嚴(yán)重的問題:
依賴層級過深在 Windows 下會出現(xiàn)問題; 同一 Package 作為其他多個不同 Package 的依賴項時,會被拷貝很多次。
為了解決這兩個問題,npm@3 重新思考了 node_modules 的結(jié)構(gòu),引入了平鋪的方案。于是就出現(xiàn)了下面我們所熟悉的結(jié)構(gòu)。
node_modules
├─ foo
| ├─ index.js
| └─ package.json
└─ bar
├─ index.js
└─ package.json
與 npm@3 不同,pnpm 使用另外一種方式解決了 npm@2 所遇到的問題,而非平鋪 node_modules。
在由 pnpm 創(chuàng)建的 node_modules 文件夾中,所有 Package 都與自身的依賴項分組在一起(隔離),但是依賴層級卻不會過深(軟鏈接到外面真正的地址)。
-> - a symlink (or junction on Windows)
node_modules
├─ foo -> .registry.npmjs.org/foo/1.0.0/node_modules/foo
└─ .registry.npmjs.org
├─ foo/1.0.0/node_modules
| ├─ bar -> ../../bar/2.0.0/node_modules/bar
| └─ foo
| ├─ index.js
| └─ package.json
└─ bar/2.0.0/node_modules
└─ bar
├─ index.js
└─ package.json
基于非扁平化的 node_modules 目錄結(jié)構(gòu),解決 Phantom dependencies。Package 只可觸達(dá)自身依賴。 通過軟鏈復(fù)用相同版本的 Package,避免重復(fù)打包(相同版本),解決 NPM doppelgnger(順帶解決磁盤占用)。
可以發(fā)現(xiàn),很多與包管理器相關(guān)的問題就此迎刃而解。
Why should we use pnpm?[10] 平鋪 node_modules 不是唯一的路[11]
Rush[12]
a scalable monorepo manager for the web
命令統(tǒng)一。
rush(x) xxx 一把梭,減少新人上手成本。同時 Rush 除了 rush add 以及 rushx xxx 等命令需要在指定項目下運行,其他命令均為全局命令,可在項目內(nèi)任意目錄執(zhí)行,避免了在終端頻繁切換項目路徑的問題。

強大的依賴分析能力。
Rush 中的許多命令支持分析依賴關(guān)系,比如 -t(to) 參數(shù):
$ rush install -t @monorepo/app1
該命令只會安裝 app1 的依賴及其 app1 依賴的 package 的依賴,即按需安裝依賴。
$ rush build -t @monorepo/app1
該命令會執(zhí)行 app1 以及 app1 依賴的 package 的構(gòu)建腳本。
類似的,還有 -f(from) 參數(shù),可以使命令只作用于當(dāng)前 package 以及依賴了該 package 的 package。
保證依賴版本一致性
Monorepo 中的項目應(yīng)當(dāng)盡量保證依賴版本的一致性,否則很有可能出現(xiàn)重復(fù)打包以及其他的問題。
Rush 則提供了許多能力來保證這一點,如rush check、rush add -p package-name -m 以及 ensureConsistentVersions。
有興趣的同學(xué)可以自行翻閱 Rush 的官方文檔,十分詳盡,對于一些常見問題也有說明。
Package 引用規(guī)范

產(chǎn)物引用
傳統(tǒng)引用方式,構(gòu)建完成后,app 直接引用 package 的構(gòu)建產(chǎn)物。開發(fā)階段可以通過構(gòu)建工具提供的能力保證實時構(gòu)建(如 tsc --watch)
優(yōu)點:規(guī)范,對 app 友好。 缺點:隨著模塊增多,package 熱更新速度可能變得難以忍受。
源碼引用
package.json 中的 main 字段配置為源文件的入口文件,引用該 package 的 app 需要將該 package 納入編譯流程。
優(yōu)點:借助 app 的熱更新能力,自身沒有生成構(gòu)建產(chǎn)物的過程,熱更新速度快 缺點:需要 app 進行適配, alias適配繁瑣;
引用規(guī)范
對于項目內(nèi)部使用的 packages ,稱為 features,不應(yīng)當(dāng)向外發(fā)布,直接將 main字段設(shè)置為源文件入口并配置 app 項目的 webpack,走后編譯形式。對于需要對外發(fā)布的 packages,不應(yīng)該也不允許引用 features,必須要有構(gòu)建過程,如果需要使用源碼開發(fā)增加熱更新速度,可以新增一個自定義的入口字段,app 的 webpack 配置中優(yōu)先識別該入口字段即可。
補充:rush build 命令是支持構(gòu)建產(chǎn)物緩存的,如果 app 拆分粒度夠小,可復(fù)用的包足夠多,同時打包鏡像支持構(gòu)建產(chǎn)物緩存的 set 與 get,就可以做到增量構(gòu)建 app。
Workspace protocol (workspace:)
在 PNPM 和 Yarn 支持 Workspace 能力之前,Rush 就誕生了。Rush 的方法是將所有軟件包集中安裝在 common / temp 文件夾中,然后 Rush 創(chuàng)建從每個項目到 common / temp 的符號鏈接。與 PNPM Workspace 本質(zhì)上是等效的。
開啟 PNPM workspace[13] 能力從而可以使用 workspace:協(xié)議保證引用版本的確定性,使用了該協(xié)議引用的 package 只會使用 Monorepo 中的內(nèi)容。
{
"dependencies": {
"foo": "workspace:*",
"bar": "workspace:~",
"qar": "workspace:^",
"zoo": "workspace:^1.5.0"
}
}
推薦引用 Monorepo 內(nèi)的 package 時統(tǒng)一使用該協(xié)議,引用本地最新版本內(nèi)容,保證更改能夠及時擴散同步至其他項目,這也是 Monorepo 的優(yōu)勢所在。
若一定要使用遠(yuǎn)端版本,需要在 rush.json 中配置具體 project (增加 cyclicDependencyProjects配置),詳見 rush_json[14]。
很幸運的是 PNPM workspace 中 workspace:* 可以匹配 prerelease 版本 ?? Local prerelease version of packages should be linked only if the range is \*[15]
問題記錄
Monorepo Project Dependencies Duplicate
這個問題類似于前面提到的 Yarn duplicate,但并不是 Yarn 獨有的。
假設(shè)存在以下依賴關(guān)系(將 Yarn duplicate 的例子進行改造,放在 Monorepo 場景中)
app1 以及 package1 同屬于 Monorepo 內(nèi)部 project。

在 Rush(pnpm)/Yarn 項目中,會嚴(yán)格按照 Monorepo 內(nèi) project 的 package.json 所聲明的版本進行安裝,即 app1 安裝 [email protected],package1 安裝 [email protected]。
此時對 app1 進行打包,則 [email protected] 和 [email protected] 都會被打包。
對這個結(jié)果你也許會有一些意外,但仔細(xì)想想,又很自然。
換一種方式理解,整個 Monorepo 是一個大的虛擬 project,我們所有的 project 都作為這個虛擬 project 的直接依賴存在。
{
"name": "fake-project",
"version": "1.0.0",
"dependencies": {
"@fake-project/app1": "1.0.0",
"@fake-project/package1": "1.0.0"
}
}
安裝依賴時,(p)npm 首先下載直接依賴項,然后再下載間接依賴項,并且在安裝到相同模塊時,判斷已安裝的模塊版本(直接依賴項)是否符合新模塊(間接依賴項)的版本范圍,如果符合則跳過,不符合則在當(dāng)前模塊的 node_modules 下安裝該模塊。
而 app1 和 package1 的直接依賴關(guān)系 lib-a 是該 fake-project 的間接依賴項,無法滿足上述判斷條件,于是按照對應(yīng) package.json 中描述的版本安裝。
解決方案:Rush: Preferred versions[16]
Rush 可以通過手動指定 preferredVersions 的方式,避免兩個可兼容版本的重復(fù)。這里將 Monorepo 中 lib-a 的 preferredVersions 指定為 1.2.0,相當(dāng)于在該虛擬 project 下直接安裝了指定的版本的模塊,作為直接依賴項。
{
"name": "fake-project",
"version": "1.0.0",
"dependencies": {
"@fake-project/app1": "1.0.0",
"@fake-project/package1": "1.0.0",
"lib-a": "1.1.0"
}
}
對于 Yarn,由于 Yarn duplicate 的存在,就算在根目錄指定安裝確定版本的 lib-a 也是無效的。但是依舊有兩種方案可以進行處理:
通過 yarn-deduplicate[17] 針對性的修改 yarn.lock;使用 resolutions字段。過于粗暴,不像preferredVersions可以允許不兼容版本的存在,不推薦。
需要謹(jǐn)記:在 Yarn 下消除重復(fù)依賴,也應(yīng)該一個 Package 一個 Package 的去進行處理,小心使得萬年船。
對于存在副作用的公共庫,版本最好保持統(tǒng)一; 對于其他的體積?。ɑ蛑С职葱杓虞d)、無副作用的公共庫,重復(fù)打包在一定程度上可以接受的。
prettier
由于根目錄不再存在 node_modules,故需要每個項目安裝一個 prettier 作為 devDependency 并編寫 .prettierrc.js 文件。
本著偷懶的原則,根目錄新建 .prettierrc.js(不依賴任何第三方包),全局安裝 prettier 解決該問題。
eslint
先看一個場景,若在項目中使用 eslint-config-react-app,除了需要安裝 eslint-config-react-app,還需要安裝一系列 peerDependencies 插件。


為什么 eslint-config-react-app 不將這一系列插件作為 dependencies 內(nèi)置,而是作為 peerDependencies?使用者并不需要關(guān)心預(yù)設(shè)配置內(nèi)引用了哪些插件。
具體討論可以查看該 issue,里面有相關(guān)問題的討論:Support having plugins as dependencies in shareable config #3458[18] 。
總而言之:和 eslint 插件的具體查找方式有關(guān),如果因為依賴提升失?。ǘ喟姹緵_突),導(dǎo)致需要的插件被裝在了非根目錄 node_modules 下,就可能產(chǎn)生問題,而用戶自行安裝 peerDependencies 可以保證不會出現(xiàn)該問題。
當(dāng)然,我們也發(fā)現(xiàn)一些開源的 eslint 預(yù)設(shè)配置不需要安裝 peerDependencies,這些預(yù)設(shè)利用了 yarn 和 npm 的扁平 node_modules 結(jié)構(gòu),也就是依賴提升,裝的包都被提升至根目錄 node_modules,故可以正常運作。即便如此,在基于 Yarn 的 Monorepo 中,依賴一旦復(fù)雜起來,就可能出現(xiàn)插件無法被查找到的情況,能夠正常運轉(zhuǎn)就像一個有趣的巧合。
在 Rush 中,不存在依賴提升(提升也不一定靠譜),裝一系列的插件又過于繁瑣,則可以通過打補丁[19]的方式繞過。
git hooks
通常會在項目中使用 husky 注冊 pre-commit 和 commit-msg 鉤子,用于校驗代碼風(fēng)格以及 commit 信息。
很明顯,在 Rush 項目的結(jié)構(gòu)下,根目錄是沒有 node_modules 的,無法直接使用 husky。
我們可以借助 rush init-autoinstaller[20] 的能力來達(dá)到一樣的效果,本節(jié)主要參考官方文檔 Installing Git hooks[21] 以及 Enabling Prettier[22] 的內(nèi)容。
# 初始化一個名為 rush-lint 的 autoinstaller
$ rush init-autoinstaller --name rush-lint
$ cd common/autoinstallers/rush-lint
# 安裝 lint 所需依賴
$ pnpm i @commitlint/cli @commitlint/config-conventional @microsoft/rush-lib eslint execa prettier lint-staged
# 更新 rush-lint 的 pnpm-lock.yaml
$ rush update-autoinstaller --name rush-lint
在 rush-lint 目錄下新增 commit-lint.js 以及 commitlint.config.js,內(nèi)容如下
commit-lint.js
const path = require('path');
const fs = require('fs');
const execa = require('execa');
const gitPath = path.resolve(__dirname, '../../../.git');
const configPath = path.resolve(__dirname, './commitlint.config.js');
const commitlintBinPath = path.resolve(__dirname, './node_modules/.bin/commitlint');
if (!fs.existsSync(gitPath)) {
console.error('no valid .git path');
process.exit(1);
}
main();
async function main() {
try {
await execa('bash', [commitlintBinPath, '--config', configPath, '--cwd', path.dirname(gitPath), '--edit'], {
stdio: 'inherit',
});
} catch (\_e) {
process.exit(1);
}
}
commitlint.config.js
const rushLib = require("@microsoft/rush-lib");
const rushConfiguration = rushLib.RushConfiguration.loadFromDefaultLocation();
const packageNames = [];
const packageDirNames = [];
rushConfiguration.projects.forEach((project) => {
packageNames.push(project.packageName);
const temp = project.projectFolder.split("/");
const dirName = temp[temp.length - 1];
packageDirNames.push(dirName);
});
// 保證 scope 只能為 all/package name/package dir name
const allScope = ["all", ...packageDirNames, ...packageNames];
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
"scope-enum": [2, "always", allScope],
},
};
注意:此處不需要新增 prettierrc.js(根目錄已存在) 以及 eslintrc.js(各項目已存在)。
根目錄新增 .lintstagedrc 文件
.lintstagedrc
{
"{apps,packages,features}/**/*.{js,jsx,ts,tsx}": [
"eslint --fix --color",
"prettier --write"
],
"{apps,packages,features}/**/*.{css,less,md}": ["prettier --write"]
}
完成了相關(guān)依賴的安裝以及配置的編寫,我們接下來將相關(guān)命令執(zhí)行注冊在 rush 中。
修改 common/config/rush/command-line.json 文件中的 commands 字段。
{
"commands": [
{
"name": "commitlint",
"commandKind": "global",
"summary": "Used by the commit-msg Git hook. This command invokes commitlint to lint commit message.",
"autoinstallerName": "rush-lint",
"shellCommand": "node common/autoinstallers/rush-lint/commit-lint.js"
},
{
"name": "lint",
"commandKind": "global",
"summary": "Used by the pre-commit Git hook. This command invokes eslint to lint staged changes.",
"autoinstallerName": "rush-lint",
"shellCommand": "lint-staged"
}
]
}
最后,將 rush commitlint 以及 rush lint 兩個命令分別與 commit-msg 以及 pre-commit鉤子進行綁定。common/git-hooks 目錄下增加 commit-msg 以及 pre-commit 腳本。
commit-msg
#!/bin/sh
node common/scripts/install-run-rush.js commitlint || exit $? #++
pre-commit
#!/bin/sh
node common/scripts/install-run-rush.js lint || exit $? #++
如此,便完成了需求。
避免全局安裝 eslint 以及 prettier
經(jīng)過上一節(jié)的處理,在 rush-lint 目錄下安裝了 eslint 以及 prettier 后,我們便無需全局安裝了,只需要配置一下 VSCode 即可。
{
// ...
"npm.packageManager": "pnpm",
"eslint.packageManager": "pnpm",
"eslint.nodePath": "common/autoinstallers/rush-lint/node_modules/eslint",
"prettier.prettierPath": "common/autoinstallers/rush-lint/node_modules/prettier"
// ...
}
附錄
常用命令
| yarn | rush(x) | detail |
|---|---|---|
| yarn install | rush install | 安裝依賴 |
| yarn upgrade | rush update | rush update 安裝依賴,基于 lock 文件 rush update --full 全量更新到符合 package.json 的最新版本 |
| yarn add package-name | rush add -p package-name | yarn add 默認(rèn)安裝版本號為 ^ 開頭,可接受小版本更新 rush add 默認(rèn)安裝版本號為 ~ 開頭,僅接受補丁更新 rush add 可通過增加 --caret 參數(shù)達(dá)到與 yarn add 效果一致 rush add 不可一次性安裝多個 package |
| yarn add package-name --dev | rush add -p package-name --dev | - |
| yarn remove package-name | - | rush 不提供 remove 命令 |
| - | rush build | 執(zhí)行文件存在變更(基于 git)的項目的 build 腳本 rush build -t @monorepo/app1 表示只構(gòu)建 @monorepo/app1 及其依賴的 package rush build -T @monorepo/app1 表示只構(gòu)建 @monorepo/app1 依賴的 package,不包含其本身 |
| - | rush rebuild | 默認(rèn)執(zhí)行所有項目的 build 腳本 |
| yarn xxx(自定義腳本) | rushx xxx(自定義腳本) | yarn xxx 執(zhí)行當(dāng)前目錄下 package.json 中的 xxx 腳本(npm scripts) rushx xxx 同理。可以直接執(zhí)行 rushx 查看當(dāng)前項目所支持的腳本命令。 |
工作流
# 從 git 拉取最新變更
$ git pull
# 更新 NPM 依賴
$ rush update
# 重新打包 @monorepo/app1 依賴的項目(不含包其本身)
$ rush rebuild -T @monorepo/app1
# 進入指定項目目錄
$ cd ./apps/app1
# 啟動項目
$ rushx start # or rushx dev
參考文章
Rush.js[23] node_modules 困境[24] Why should we use pnpm?[25] 平鋪 node_modules 不是唯一的路[26]
參考資料
Yarn duplicate 及解決方案: https://github.com/worldzhao/blog/issues/10
[2]difference between npm and yarn behavior with nested dependencies #3951: https://github.com/yarnpkg/yarn/issues/3951
[3]Yarn installing multiple versions of the same package: https://stackoverflow.com/questions/49072905/yarn-installing-multiple-versions-of-the-same-package
[4]yarn-deduplicate-Cleans up yarn.lock by removing duplicates.: https://github.com/atlassian/yarn-deduplicate
[5]natively: https://github.com/yarnpkg/berry/pull/1558
[6]Workspace package with prerelease version and wildcard dep version #6719: https://github.com/yarnpkg/yarn/issues/6719
[7]Automatically resolve conflicts in lockfile · Issue #2036 · pnpm/pnpm: https://github.com/pnpm/pnpm/issues/2036
[8]node_modules 困境: https://zhuanlan.zhihu.com/p/137535779
[9]pnpm: https://pnpm.io/
[10]Why should we use pnpm?: https://www.kochan.io/nodejs/why-should-we-use-pnpm.html
[11]平鋪 node_modules 不是唯一的路: https://pnpm.io/zh/blog/2020/05/27/flat-node-modules-is-not-the-only-way
[12]Rush: https://rushjs.io/
[13]PNPM workspace: https://pnpm.io/zh/workspaces
[14]rush_json: https://rushjs.io/pages/configs/rush_json/
[15]Local prerelease version of packages should be linked only if the range is *: https://github.com/pnpm/pnpm/pull/2259
[16]Rush: Preferred versions: https://rushjs.io/pages/advanced/preferred_versions/
[17]yarn-deduplicate: https://github.com/atlassian/yarn-deduplicate
[18]Support having plugins as dependencies in shareable config #3458: https://github.com/eslint/eslint/issues/3458
[19]補丁: https://www.npmjs.com/package/@rushstack/eslint-patch
[20]rush init-autoinstaller: https://rushjs.io/pages/commands/rush_init-autoinstaller/
[21]Installing Git hooks: https://rushjs.io/pages/maintainer/git_hooks/
[22]Enabling Prettier: https://rushjs.io/pages/maintainer/enabling_prettier/
[23]Rush.js: https://rushjs.io/
[24]node_modules 困境: https://zhuanlan.zhihu.com/p/137535779
[25]Why should we use pnpm?: https://www.kochan.io/nodejs/why-should-we-use-pnpm.html
[26]平鋪 node_modules 不是唯一的路: https://pnpm.io/zh/blog/2020/05/27/flat-node-modules-is-not-the-only-way
