yarn?命令死循環(huán)問(wèn)題分析解決
前言
最近有個(gè)想法,希望在一個(gè) yarn workspace 項(xiàng)目中實(shí)現(xiàn)任意一個(gè)子包中安裝依賴時(shí),都執(zhí)行一些類似于初始化、同步配置的動(dòng)作。
然而在操作過(guò)程中遇到了一個(gè)關(guān)于 yarn --cwd
有趣的問(wèn)題,特地記錄下來(lái),希望能對(duì)后來(lái)者有所幫助。
遇到什么問(wèn)題呢
先交代一下我們項(xiàng)目的基本情況,它是一個(gè)通過(guò) yarn workspace 管理的 monorepo 項(xiàng)目,使用的是 yarn v1.22.11 版本,目錄結(jié)構(gòu)大致如下:
monorepo ├── package.json ├── app-a │ └── package.json ├── app-b │ └── package.json └── config └── package.json
其中 app-a
和 app-b
都使用了 config
這個(gè)共享包:
"dependencies": { "@monorepo/config": "../config", }
我們需要在根目錄的 package.json
中的 preinstall
鉤子做一些初始化操作:
"scripts": { "preinstall": "./bin/init.sh", }
此時(shí)我們?cè)诟夸泩?zhí)行 yarn
或者 yarn add <pkg-name>
,都會(huì)觸發(fā) preinstall
這個(gè)鉤子,但在 app-a
中執(zhí)行 yarn
是不會(huì)觸發(fā)根目錄的 preinstall
鉤子的。
因此,我們需要分別在每個(gè)子包上都加上這行,也即在每個(gè)子包安裝依賴時(shí)都執(zhí)行一下根目錄的 preinstall
命令:
"scripts": { "preinstall": "yarn --cwd ../ preinstall", }
于是,奇怪的事情就發(fā)生了,當(dāng)我在 app-a
中執(zhí)行 yarn
的時(shí)候,它停留在安裝 @monorepo/config
的階段,同時(shí)我的電腦明顯變得卡頓,于是打開 htop
一看,好家伙,滿屏都是:
4ark 40987 26.3 0.5 409250368 78624 ?? R 8:36下午 0:00.09 /usr/local/bin/node /usr/local/bin/yarn --cwd ../ preinstall
CPU 占用率直接達(dá)到 100%,嚇得我趕緊 kill 掉這些進(jìn)程:
ps aux | grep preinstall | awk '{print $2}' | xargs kill -9
分析原因
驚嚇過(guò)后,來(lái)分析一下原因,很顯然這段命令陷入了死循環(huán),導(dǎo)致越來(lái)越多進(jìn)程,于是嘗試在每個(gè)子包中都手動(dòng)執(zhí)行一遍 yarn --cwd ../ preinstall
后,發(fā)現(xiàn)一切正常,那問(wèn)題出在哪呢?
于是我再執(zhí)行了一遍 yarn
,并且用以下命令將進(jìn)程信息復(fù)制出來(lái),以便分析:
ps -ef | pbcopy
隨后驗(yàn)證我剛剛的猜測(cè),的確是這個(gè)命令在不斷觸發(fā)自己,導(dǎo)致死循環(huán):
UID PID PPID C STIME TTY TIME CMD 501 50399 50379 0 8:50下午 ?? 0:00.10 /usr/local/bin/node /usr/local/bin/yarn --cwd ../ preinstall 501 50400 50399 0 8:50下午 ?? 0:00.11 /usr/local/bin/node /usr/local/bin/yarn --cwd ../ preinstall 501 50401 50400 0 8:50下午 ?? 0:00.11 /usr/local/bin/node /usr/local/bin/yarn --cwd ../ preinstall 501 50402 50401 0 8:50下午 ?? 0:00.12 /usr/local/bin/node /usr/local/bin/yarn --cwd ../ preinstall
由于三個(gè)分包執(zhí)行的命令都一樣,不清楚是不是由于某個(gè)分包引起,于是修改一下命令以便區(qū)分:
"scripts": { "preinstall": "echo app-a && yarn --cwd ../ preinstall", }
隨后發(fā)現(xiàn)問(wèn)題是出現(xiàn)在 config
這個(gè)子包,于是我把這個(gè)子包的 preinstall
命令去掉,果然沒(méi)有這個(gè)問(wèn)題了,非常奇怪。
難道是 --cwd ../
這個(gè)路徑有問(wèn)題?驗(yàn)證一下,把命令改成這樣:
"scripts": { "preinstall": "pwd && yarn --cwd ../ preinstall", }
發(fā)現(xiàn) pwd
輸出是這樣子的:
/4ark/projects/monorepo/app-a/node_modules/@monorepo/config
從這里的輸出我們發(fā)現(xiàn)了兩個(gè)問(wèn)題,第一個(gè)問(wèn)題是:
- yarn workspace 共享包的
preinstall
被執(zhí)行的時(shí)候,其實(shí)已經(jīng)被拷貝到app-a
的node_modules
中,而不是在當(dāng)前目錄,因此--cwd ../
并不指向項(xiàng)目根目錄。
這一點(diǎn)比較好理解,畢竟 config
作為一個(gè)依賴包,確實(shí)應(yīng)該被拷貝到應(yīng)用的 node_modules
。
而第二個(gè)問(wèn)題就不太理解了,為什么明明設(shè)置了 --cwd ../
,卻依然在當(dāng)前目錄執(zhí)行呢?按照預(yù)期 cwd 的指向應(yīng)該是:
/4ark/projects/monorepo/app-a/node_modules/@monorepo
難道是我對(duì) cwd 參數(shù)的理解有偏差?看一下 yarn 的文檔中對(duì) cwd 描述:
Specifies a current working directory, instead of the default ./. Use this flag to perform an operation in a working directory that is not the current one.
This can make scripts nicer by avoiding the need to cd into a folder and then cd back out.
從文檔的描述來(lái)看,cwd 的作用不就是代替 cd
嗎,但現(xiàn)在的結(jié)果看來(lái) yarn --cwd ../ preinstall
并不等價(jià)于 cd ../ && yarn preinstall
。
這就不得不讓人疑惑 cwd 的定位方式了,在網(wǎng)上搜尋一番沒(méi)找到相關(guān)的討論,那只能自己動(dòng)手豐衣足食,直接從 yarn 源碼中尋找答案。
分析源碼
前面我們說(shuō)到,我們使用的是 yarn v1.22.11,在 yarn 的 GitHub 倉(cāng)庫(kù)中發(fā)現(xiàn) v1 版本的最新版本停留在 v1.23.0-0,那我們就從這個(gè)版本的源碼來(lái)進(jìn)行分析,首先克隆代碼到本地:
git clone --depth=1 https://github.com/yarnpkg/yarn
然后安裝依賴并運(yùn)行起來(lái):
yarn && yarn watch
這時(shí)候它就會(huì)自動(dòng)監(jiān)聽代碼修改然后重新編譯,我們查看 package.json
發(fā)現(xiàn) yarn 的 bin 主要是調(diào)用 ./bin/yarn.js
:
"bin": { "yarn": "./bin/yarn.js", "yarnpkg": "./bin/yarn.js" },
也就是我們直接執(zhí)行 bin/yarn.js
的效果就如同執(zhí)行 yarn
,試一下查看版本:
> /Users/4ark/projects/yarn/bin/yarn -v 1.23.0-0
PS:當(dāng)然你也可以在項(xiàng)目目錄下使用 npm link
把它掛載到本地中。
接下就是一番調(diào)試,終于定位到可以回答我們疑問(wèn)的代碼,在這里:
function findProjectRoot(base: string): string { let prev = null; let dir = base; do { if (fs.existsSync(path.join(dir, constants.NODE_PACKAGE_JSON))) { return dir; } prev = dir; dir = path.dirname(dir); } while (dir !== prev); return base; } const cwd = command.shouldRunInCurrentCwd ? commander.cwd : findProjectRoot(commander.cwd);
可以看到 cwd 的定位方式是從當(dāng)前目錄尋找是否存在 package.json
,若存在,則返回此目錄,否則將目錄經(jīng)過(guò) path.dirname
處理一遍,繼續(xù)尋找,直到尋找到最外層。
那么這里最關(guān)鍵的是 path.dirname
的返回值,我們先看一下文檔對(duì)于它的描述:
The path.dirname() method returns the directory name of a path, similar to the Unix dirname command. Trailing directory separators are ignored,
就是返回一個(gè)路徑中的目錄部分,作用與 unix 下的 dirname 命令一致,通常是這么使用的:
> dirname /4ark/app/index.js /4ark/app > dirname /4ark/app/packages/index.js /4ark/app/packages
是不是會(huì)膚淺地認(rèn)為它的作用就是返回一個(gè)路徑的上一級(jí)目錄?如果傳入的是一個(gè)絕對(duì)路徑,確實(shí)可以這么膚淺地認(rèn)為,然而當(dāng)傳入的是一個(gè)相對(duì)路徑時(shí),情況就不一樣了:
> dirname ../app/index.js ../app > dirname ../../ ../ > dirname ../
問(wèn): 會(huì)返回什么呢?
答案是:.
,也就是當(dāng)前目錄。
那這里就能回答我們之前的問(wèn)題,為什么在 node_module/@monorepo/config
中使用 yarn --cwd ../ preinstall
卻在當(dāng)前目錄執(zhí)行,因?yàn)樗纳弦患?jí) node_modules/@monorepo
不存在 package.json
,所以經(jīng)過(guò) dirname ../
處理后 cwd 的指向就是當(dāng)前目錄。
如果對(duì) node.js 中 path.dirname
的實(shí)現(xiàn)方式感興趣,可以看這里 path.js#L538-L554。
解決方案
摸清楚原因后,那解決這個(gè)問(wèn)題也不是難事,只要我們把相對(duì)路徑改成絕對(duì)路徑,是不是就能解決這個(gè)問(wèn)題了?
思考一下,其實(shí) yarn --cwd ../ preinstall
,把 ../
改成絕對(duì)路徑行不行呢?比如在本文的場(chǎng)景,../
其實(shí)就是項(xiàng)目的根目錄,那我們完全可以通過(guò)別的方式獲取到項(xiàng)目的根目錄,比如 在 git 中:
git rev-parse --show-toplevel
所以,我們把命令改成這樣,問(wèn)題就迎刃而解了:
- yarn --cwd ../ preinstall + yarn --cwd $(git rev-parse --show-toplevel) preinstall
那就不得不提一下,其實(shí)在 yarn v2 中新增了一個(gè) --top-level
屬性,它的作用剛好就是為了解決這個(gè)問(wèn)題。
結(jié)語(yǔ)
其實(shí)我們?cè)倩剡^(guò)頭來(lái)想,在本文的例子中,根本不需要在 config
目錄中添加 preinstall
這個(gè)鉤子,因?yàn)樗鳛楣蚕戆?,每次修改都必然要在其它使用這個(gè)包的地方,重新安裝一次,所以只要確保這些地方會(huì)執(zhí)行 preinstall
就可以了,那也就意味著不會(huì)出現(xiàn)本文遇到的問(wèn)題。
不過(guò),多踩坑也不是壞事,只要搞清楚背后的原因,問(wèn)題也就不是問(wèn)題。
以上就是yarn 命令死循環(huán)問(wèn)題分析解決的詳細(xì)內(nèi)容,更多關(guān)于yarn 命令死循環(huán)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
node.js結(jié)合webSocket實(shí)現(xiàn)聊天室
于Node.js和WebSocket的聊天室,主要包括前端頁(yè)面,主要是用戶操作的頁(yè)面,還包括后臺(tái)數(shù)據(jù)通信以及邏輯處理,具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08nodejs實(shí)現(xiàn)黑名單中間件設(shè)計(jì)
最近寫的項(xiàng)目中涉及到防止灌水的功能,于是設(shè)計(jì)了黑名單中間件,跟大家分享一下,同時(shí)也希望大家有好的建議能夠拍磚.2014-06-06使用 Node.js 模擬滑動(dòng)拼圖驗(yàn)證碼操作的示例代碼
本篇文章主要介紹了使用 Node.js 模擬滑動(dòng)驗(yàn)證碼操作的示例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-11-11詳解如何使用Node.js編寫命令工具——以vue-cli為例
本篇文章主要介紹了如何使用Node.js編寫命令工具——以vue-cli為例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06詳解Nodejs 通過(guò) fs.createWriteStream 保存文件
本篇文章主要介紹了Nodejs 通過(guò) fs.createWriteStream 保存文件,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10