1057 字
5 分鐘

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。先從同一個工作目錄收集:

Terminal window
pnpm --version
pnpm config get shellEmulator
rg -n "shellEmulator|MY_VAR" pnpm-workspace.yaml package.json packages

Issue 的最小概念範例是:workspace 開啟 shell emulator,script 用 Node 印出收到的第一個參數:

pnpm-workspace.yaml
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 + emulatorhellohellohello
pnpm 12.0.0 + emulatorhello原樣傳入原樣傳入
pnpm 12.4.0 + emulatorhello原樣傳入原樣傳入
pnpm 12.4.0,不開 emulatorhellohellohello

最危險的訊號是「命令仍然成功」。對 Node 來說,原樣字串仍然是合法參數,所以 CI 可能綠燈,但實際傳給工具的路徑、環境或 fallback 已經錯了。這也是為什麼只看 exit code 不夠。

三條處理路徑#

路徑 A:不需要模擬器就關閉#

如果專案沒有依賴 shell emulator 的跨平台行為,先在分支測試:

pnpm-workspace.yaml
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 驗證值真的被展開:

Terminal window
MY_VAR=hello pnpm run --silent probe
test "$(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 Docs:Settings

pnpm GitHub issue #14814:shell emulator does not expand ${VAR}

pnpm v12.4.0 release

pnpm 12 shellEmulator 不展開 ${VAR}?環境變數排錯
https://laplusda.com/posts/pnpm-shell-emulator-variable-expansion/
作者
Zero
發佈於
2026-09-12
許可協議
CC BY-NC-SA 4.0
這篇文章有幫助嗎?

回報錯字、失效連結,或告訴我你想看的延伸主題。