pnpm 12 shellEmulator 不展開 ${VAR}?環境變數排錯
如果 pnpm script 裡的 ${VAR} 沒有被展開,卻沒有任何 non-zero exit code,不要先把問題歸類成 shell 寫錯。pnpm issue #14814 回報了一個 12.x 的 shellEmulator 回歸:bare $VAR 可以展開,但 brace 或 default syntax 可能原樣傳入 script。
直接答案是:不需要 pnpm shell 模擬器時,先關閉 shellEmulator;需要保留時,暫時避免 ${VAR} 與 ${VAR:-fallback},把 fallback 移到 Node script 或在外層 shell 預先決定,並用輸出 assertion 捕捉靜默錯誤。 這篇的版本差異取自公開 issue;我沒有在本專案執行該重現矩陣。
先確認設定真的有開
shellEmulator 是 workspace 設定,通常放在 pnpm-workspace.yaml。先從同一個工作目錄收集:
pnpm --versionpnpm config get shellEmulatorrg -n "shellEmulator|MY_VAR" pnpm-workspace.yaml package.json packagesIssue 的最小概念範例是:workspace 開啟 shell emulator,script 用 Node 印出收到的第一個參數:
shellEmulator: true{ "scripts": { "probe:bare": "node -e \"console.log(process.argv[1])\" $MY_VAR", "probe:braced": "node -e \"console.log(process.argv[1])\" ${MY_VAR}", "probe:default": "node -e \"console.log(process.argv[1])\" ${MY_VAR:-fallback}" }}上面程式碼是縮小後的說明範例,不是本專案現有設定。實際排錯時要同時記錄 pnpm、Node、作業系統與 shellEmulator 的有效值,避免只換一個版本就失去對照組。
已知 issue 的觀察結果
以下表格是 issue #14814 回報的觀察,不是本機測試結果:
| 執行條件 | $MY_VAR | ${MY_VAR} | ${MY_VAR:-fallback} |
|---|---|---|---|
| pnpm 11.26.0 + emulator | hello | hello | hello |
| pnpm 12.0.0 + emulator | hello | 原樣傳入 | 原樣傳入 |
| pnpm 12.4.0 + emulator | hello | 原樣傳入 | 原樣傳入 |
| pnpm 12.4.0,不開 emulator | hello | hello | hello |
最危險的訊號是「命令仍然成功」。對 Node 來說,原樣字串仍然是合法參數,所以 CI 可能綠燈,但實際傳給工具的路徑、環境或 fallback 已經錯了。這也是為什麼只看 exit code 不夠。
三條處理路徑
路徑 A:不需要模擬器就關閉
如果專案沒有依賴 shell emulator 的跨平台行為,先在分支測試:
shellEmulator: false關閉後由作業系統 shell 處理變數展開;這通常能恢復 brace 與 default syntax,但也表示 Windows、macOS、Linux 的 script 差異重新由你負責。不要只在 macOS 本機確認,就假設所有 CI runner 都一樣。
路徑 B:保留 emulator,改用 bare variable
如果只需要非空變數,可以把 script 改成 $MY_VAR,再在 script 前面明確檢查:
{ "scripts": { "probe": "node -e \"if (!process.env.MY_VAR) process.exit(2); console.log(process.env.MY_VAR)\" $MY_VAR" }}這個 workaround 沒有 default value 語意;變數缺少時要由 Node、CI 或呼叫端決定是否失敗。對含空白、引號或路徑的值,也要另外測試 quoting,不要只測 hello。
路徑 C:把 fallback 移出 shell
需要 ${VAR:-fallback} 類似行為時,把規則移到可測試的 Node script:
const value = process.env.MY_VAR || "fallback";console.log(value);|| 會把空字串也視為 fallback;如果只想在變數未設定時套用預設,改用 process.env.MY_VAR ?? “fallback”。這樣 fallback 規則不依賴 pnpm 的 shell parser,也比較容易在單元測試覆蓋。
另一個選擇是在啟動 pnpm 前由外層、已知可展開 ${VAR} 的 shell 先設定環境變數,再讓 script 只讀 $MY_VAR。這能縮小 pnpm parser 的責任,但仍要把 shell、runner 與 quoting 寫進 CI contract。
用輸出 assertion 抓住靜默錯誤
先建立一個只輸出固定 sentinel 的 probe,再在 CI 驗證值真的被展開:
MY_VAR=hello pnpm run --silent probetest "$(MY_VAR=hello pnpm run --silent probe)" = "hello"如果 script 會輸出額外 log,請改用 Node 直接讀取 JSON 或在 probe 中明確印出單一欄位。測試至少包含:
- 變數有值。
- 變數未設定。
- 變數是空字串。
- 值含空白、引號與路徑分隔符。
- pnpm 11、目前 pin 的 pnpm 12、CI 使用的 Node 與 runner。
不要把 pnpm run 的成功當成展開成功;要 assertion 實際收到的 argument。
升級前要看 issue 與 release
本次查核時,issue #14814 仍是公開的 open bug,回報版本包含 pnpm 12.0.0 與 12.4.0;這不代表所有 12.x 都一定有相同結果,也不代表未來版本不會修正。正式決定前,請重新查看 issue、release note 與自己的最小重現,不要只因版本號變大就推論已修好。
如果你遇到的是啟動即 SyntaxError,而不是變數原樣傳入,請改看 pnpm 12.3.0 的 packageManager shim 排錯,那是另一條啟動鏈問題。
參考資料:
pnpm GitHub issue #14814:shell emulator does not expand ${VAR}
回報錯字、失效連結,或告訴我你想看的延伸主題。