把 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

改了什麼#

位置升級前升級後
CIactions/setup-node@v7,node-version: 24node-version: 26
sandbox 基底映像node:24.22.1-trixie-slimnode:26.12.1-trixie-slim
sandbox 的 npm11.19.111.20.0(隨 Node 附帶)
sandbox 的 pnpm12.15.0,靠 corepack12.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、?url import,不然這些檔案在 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() 直接回傳結果,不用 await
  • readFile 改成 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/null
RUN npm install --global --no-fund --no-audit [email protected] [email protected] && npm cache clean --force
RUN 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,驗證都在暫存目錄做

  1. 用 git archive 解出一份乾淨的副本
  2. 用 Node 26 跑 npm ci,508 個套件,大約 5 秒
  3. Node 26.12.1 跑 npm run check,全部通過,包含型別、樣式、工具介面快照、21 個原生外掛契約、58 個契約測試
  4. vite build 成功,大約 0.9 秒
  5. 換回 Node 24.21.0,新的 vite-imports.js 一樣通過 npm run check,所以還是相容 Node 24
  6. docker build sandbox 映像成功,進容器確認 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 那邊也支援才有意義

升級步驟#

整理成下次可以照著做的清單

  1. 先查時程:nodejs/Release 的 schedule、nodejs.org 的 release 列表,順便看專案依賴的 engines 有沒有最低版本要求
  2. 讀 changelog 的 deprecations and removals,對照專案裡實際用到的 API
  3. 不要動本機環境,在乾淨的副本裡用新版 Node 跑 npm ci 和所有檢查,看有沒有新的 warning
  4. 有 hook、shim 這種「壞了不一定會報錯」的東西,想辦法數它實際有沒有被呼叫
  5. 改完再用舊版 Node 跑一次,確認沒有讓還沒升級的人壞掉
  6. 容器的基底映像不要只換 tag,進去看裡面少了什麼、多了什麼(這次就是少了 corepack 和 yarn)
  7. 改 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 才會驗到

把 pi mobile 升上 Node.js 26:Node 只是工具鏈的時候,要看的是什麼
https://geminixiang.com/posts/pi-mobile-node-26-upgrade/
作者
Ying Xiang Zhao
發佈於
2026-10-27
許可協議
CC BY-NC-SA 4.0