把 pi mobile 升上 Node.js 26:Node 只是工具鏈的時候,要看的是什麼
Node.js 26 在 2026-05-05 發布,照 nodejs/Release 的時程,明天(2026-10-28)轉成 LTS
Node 24 則是在 2026-10-20 進入維護期,2028-04-30 EOL,所以不急,但也差不多該換了
p.s. Node 26 是最後一個照舊時程走的版本,從 Node 27 開始改成每年一個大版本、每個版本都會轉 LTS,Node 27 明天開始出 Alpha,細節可以看 nodejs/Release 的 README
這篇記錄把 pi mobile 從 Node 24 升到 26 的過程
網路上已經有不少「Node 26 有什麼新東西」的整理,這篇不重複,主要講實際遇到什麼、怎麼解、怎麼驗證
Node 在 pi mobile 裡負責什麼
先講背景,因為這決定了升級要看哪裡
pi mobile 的 app 跑在 Android 的 WebView 裡,接電腦用的 host 是 Go 寫的,這兩個都不用 Node
Node 只出現在三個地方
- 建置和檢查:tsc、stylelint、用 node 直接跑的
.ts測試、vite build - CI:發 release 的 GitHub Actions workflow
- sandbox 容器:agent 在使用者電腦上執行指令的 Linux 容器,裡面要有 Node 給它用
所以這次升級,重點在工具鏈和容器,不在新的 API
改了什麼
| 位置 | 升級前 | 升級後 |
|---|---|---|
| CI | actions/setup-node@v7,node-version: 24 | node-version: 26 |
| sandbox 基底映像 | node:24.22.1-trixie-slim | node:26.12.1-trixie-slim |
| sandbox 的 npm | 11.19.1 | 11.20.0(隨 Node 附帶) |
| sandbox 的 pnpm | 12.15.0,靠 corepack | 12.15.0,改用 npm 全域安裝 |
| 文件、給模型的系統提示 | 寫 Node 24 | 改成 Node 26 |
另外 sandbox 裡的 [email protected] 在 engines 寫的是 ^22.23.0 || ^24.22.0 || ^26.12.0,所以 Node 26 要用 26.12 以上,選了目前最新的 26.12.1(2026-10-21 發布)
本機開發環境這次沒動,還是 nvm 的 Node 24,測試時另外下載官方的 26.12.1 tarball 來跑,SHA256 先跟官方的 SHASUMS256.txt 對過
Node 26 的變化,跟這次有關的只有兩個
Node 26.0.0 的 changelog 列的重點大概是
- Temporal API 預設開啟
- V8 升到 14.6,多了
Map.prototype.getOrInsert()、Iterator.concat()等 - Undici 升到 8
- 移除
http.Server.prototype.writeHeader()、舊的_stream_*模組、--experimental-transform-types module.register()變成 runtime deprecation(DEP0205)
對 pi mobile 有影響的只有最後一個
另一個有影響的不是 Node 26 才有的:Node 25 開始就不再附帶 corepack(nodejs/node#57617),從 24 直接跳 26 的人會一起遇到
問題一:module.register() 被 deprecate
升上去之後,每一支用 node --import ./test/vite-imports.js 跑的檢查都會多印一行
(node:18522) [DEP0205] DeprecationWarning: `module.register()` is deprecated. Use `module.registerHooks()` instead.檢查本身還是會過,只是很吵,而且官方文件寫明之後會移除
test/vite-imports.js 是一個 module hook,做兩件事
- 讓 Node 看得懂 Vite 的
?raw、?urlimport,不然這些檔案在 node 直接跑測試的時候會 import 失敗 - 讓 pi-env 裡的
node:child_process改走一個 shim
原本用 register() 掛 hook,hook 要寫成字串、包在 data: URL 裡,因為它會跑在另一個 loader thread
register( "data:text/javascript," + encodeURIComponent(` import { readFile } from "node:fs/promises"; export async function resolve(specifier, context, next) { ... } export async function load(url, context, next) { ... } `), import.meta.url,);改成 module.registerHooks() 之後,hook 就是同一個檔案裡的普通同步函式
registerHooks({ resolve(specifier, context, next) { const match = viteQuery.exec(specifier); if (!match) return next(specifier, context); return { url: next(match[1], context).url + "?" + match[2], shortCircuit: true, format: "module" }; }, load(url, context, next) { const match = viteQuery.exec(url); if (!match) return next(url, context); const value = match[2] === "raw" ? readFileSync(new URL(match[1]), "utf8") : match[1]; return { format: "module", source: "export default " + JSON.stringify(value) + ";", shortCircuit: true }; },});(child_process 的那段 resolve 省略了)
差別是
next()直接回傳結果,不用 awaitreadFile改成readFileSync- 不用再把程式碼塞進字串,編輯器看得到,也比較好讀
文件上 registerHooks() 是 v23.5.0、v22.15.0 加入的,所以這個改法 Node 24 也能跑,不用等大家都升完才改
DEP0205 的說明也講了為什麼要換:非同步、跑在另一條 thread 的 hook 牽涉 worker 協調,有些問題一直解不掉
怎麼確認 shim 還有生效
這種 hook 改壞了不一定會報錯,可能只是默默沒套用
所以讓 shim 每次被用到就印一行,再把用到這個 loader 的 5 支檢查逐一跑過,數命中次數
- 舊 hook + Node 24
- 新 hook + Node 24
- 新 hook + Node 26
三種組合結果都一樣:tools-node 2 次、contracts 2 次,其他三支 0 次
問題二:Node 26 沒有 corepack 了
sandbox 原本用 corepack 拿 pnpm
RUN corepack enable pnpm && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 pnpm --version > /dev/null但 node:26.12.1-trixie-slim 裡面找不到 corepack,也沒有 yarn;node:24.22.1-trixie-slim 裡兩個都有
所以這行在 Node 26 上不能用
這是看映像內容推出來的,沒有真的拿舊的 Dockerfile 建一次看它失敗
解法是刪掉那行,把 pnpm 加進原本就有的 npm install --global,跟其他全域工具一樣釘版本
RUN corepack enable pnpm && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 pnpm --version > /dev/nullRUN npm install --global --no-fund --no-audit [email protected] [email protected] && npm cache clean --forceRUN npm install --global --no-fund --no-audit [email protected] [email protected] [email protected] && npm cache clean --force另一條路是 npm install -g corepack 把它裝回來,但只為了拿 pnpm 多裝一個套件,而且其他工具都是直接釘版本,就沒走這條,也沒實測
yarn 沒有補裝,因為文件和給模型的提示一直都只寫 npm 和 pnpm
問題三:npm 11.20 的 install-scripts 警告
Node 26.12 附帶的 npm 是 11.20.0,npm ci 多了這個警告
npm warn install-scripts 4 packages have install scripts not yet covered by allowScripts:列的是 @google/genai、@mongodb-js/zstd、fsevents、protobufjs
docker 裡的全域安裝也有一樣的警告,列的是 pnpm、esbuild、workerd
npm help install-scripts 寫的是 “Dependency install scripts are blocked by default”,但實際觀察 11.20.0 還是有執行這些腳本:pnpm 和 esbuild 裝完之後都被換成 ELF 原生執行檔
所以目前只是警告,沒有加 allowScripts
推測:如果哪天 npm 真的開始擋,pnpm 會退回透過 Node 執行的版本,esbuild 和 workerd 應該也有 JS 的退路,但這部分沒有實測,到時候要再確認一次
沒變的:fetch 還是不走 HTTPS_PROXY
sandbox 的對外流量要經過 proxy,之前就有設 NODE_USE_ENV_PROXY=1
想說 Node 26 會不會預設就吃 HTTPS_PROXY 了,測了一下:設 HTTPS_PROXY=http://127.0.0.1:9(一個沒人在聽的 port),對 https://example.com 發 fetch
- 沒設
NODE_USE_ENV_PROXY:回 200,代表直連,沒走 proxy - 設了
NODE_USE_ENV_PROXY=1:ECONNREFUSED,代表有走 proxy
所以這個環境變數還是要留著,文件上它目前也還是 Stability 1.1(Active Development)
驗證
為了不動到共用工作區的 node_modules,也不改本機的 nvm,驗證都在暫存目錄做
- 用
git archive解出一份乾淨的副本 - 用 Node 26 跑
npm ci,508 個套件,大約 5 秒 - Node 26.12.1 跑
npm run check,全部通過,包含型別、樣式、工具介面快照、21 個原生外掛契約、58 個契約測試 vite build成功,大約 0.9 秒- 換回 Node 24.21.0,新的
vite-imports.js一樣通過npm run check,所以還是相容 Node 24 docker buildsandbox 映像成功,進容器確認 node v26.12.1、npm 11.20.0、pnpm 12.15.0,cf、wrangler、az、gcloud、gh 都能用;esbuild 能轉 TS;pnpm add is-number能裝也能 require
p.s.
git archive解出來的副本沒有.git,而檢查裡有跑 git,所以會報fatal: not a git repository,在副本裡git init一下就好,跟 Node 26 無關
還沒驗到的
- CI:release workflow 只有推 tag 才會跑,要等下一次發版
- Node 24 和 26 的建置產物是不是逐位元相同,沒比
- 沒有透過手機 app 的流程實際建一次 sandbox;app 是用 Dockerfile 的 sha256 判斷要不要重建,所以下次開 sandbox 時會自動重建
Node 26 的新功能呢
Temporal、Map.getOrInsert()、Iterator.concat()、Undici 8,這次都沒有用,也沒評估
原因很單純:pi mobile 的程式跑在 Android WebView,不是跑在 Node
在 app 的程式裡用了 Node 才有的東西,電腦上測試會過,到手機上才壞
對這種「Node 只是工具鏈」的專案,升級 Node 要看的是
- 建置和測試用到的 Node API 有沒有被 deprecate 或移除
- CI 和容器裡的 Node 版本
- 跟著 Node 一起被換掉的東西:npm 版本、corepack 之類
新的語言功能,要等 WebView 那邊也支援才有意義
升級步驟
整理成下次可以照著做的清單
- 先查時程:nodejs/Release 的 schedule、nodejs.org 的 release 列表,順便看專案依賴的
engines有沒有最低版本要求 - 讀 changelog 的 deprecations and removals,對照專案裡實際用到的 API
- 不要動本機環境,在乾淨的副本裡用新版 Node 跑
npm ci和所有檢查,看有沒有新的 warning - 有 hook、shim 這種「壞了不一定會報錯」的東西,想辦法數它實際有沒有被呼叫
- 改完再用舊版 Node 跑一次,確認沒有讓還沒升級的人壞掉
- 容器的基底映像不要只換 tag,進去看裡面少了什麼、多了什麼(這次就是少了 corepack 和 yarn)
- 改 CI 的
node-version、文件、所有寫死版本號的地方,包括給 agent 的系統提示
結
- 對 Node 只是工具鏈的專案,升級的重點在工具鏈和容器,不在新 API
module.register()換成module.registerHooks()程式反而變簡單,而且 Node 22.15、23.5 之後的版本都有,可以先改- 從 Node 24 跳上來的話,記得 corepack 從 Node 25 就沒有了,Docker 映像裡用 corepack 拿 pnpm、yarn 的要改
- npm 的 install-scripts 目前只是警告,但遲早要面對
- fetch 要走 proxy,還是得設
NODE_USE_ENV_PROXY=1
還沒做的:本機要自己 nvm install 26、sandbox 少了 yarn、npm 真的開始擋 install scripts 時要重新確認 pnpm、esbuild、workerd,以及 CI 要等下次推 tag 才會驗到