---
title: "把 pi mobile 升上 Node.js 26：Node 只是工具鏈的時候，要看的是什麼"
url: https://geminixiang.com/posts/pi-mobile-node-26-upgrade/
description: "Node 26 明天轉 LTS，我把 pi mobile 的 CI、sandbox 容器和開發環境從 Node 24 升上去。記錄實際遇到的三件事：module.register() 被 runtime deprecate、Node 25 起不再附帶 corepack、npm 11.20 的 install-scripts 警告，還有怎麼驗證、哪些還沒驗到。"
published: 2026-10-27
category: "DevOps"
tags: ["Node.js", "npm", "Docker", "pi mobile", "升級"]
---

# 把 pi mobile 升上 Node.js 26：Node 只是工具鏈的時候，要看的是什麼

Node.js 26 在 2026-05-05 發布，照 [nodejs/Release](https://github.com/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](https://github.com/geminixiang/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 裡的 `cf@1.0.0-beta.23` 在 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](https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V26.md#26.0.0) 列的重點大概是

- 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](https://github.com/nodejs/node/pull/57617)），從 24 直接跳 26 的人會一起遇到

## 問題一：module.register() 被 deprecate

升上去之後，每一支用 `node --import ./test/vite-imports.js` 跑的檢查都會多印一行

```text
(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

```js
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 就是同一個檔案裡的普通同步函式

```js
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`
- 不用再把程式碼塞進字串，編輯器看得到，也比較好讀

[文件](https://nodejs.org/api/module.html#moduleregisterhooksoptions)上 `registerHooks()` 是 v23.5.0、v22.15.0 加入的，所以這個改法 Node 24 也能跑，不用等大家都升完才改

[DEP0205](https://nodejs.org/api/deprecations.html#dep0205-moduleregister) 的說明也講了為什麼要換：非同步、跑在另一條 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

```dockerfile
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`，跟其他全域工具一樣釘版本

```diff
-RUN corepack enable pnpm && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 pnpm --version > /dev/null
-RUN npm install --global --no-fund --no-audit cf@1.0.0-beta.23 wrangler@4.153.0 && npm cache clean --force
+RUN npm install --global --no-fund --no-audit pnpm@12.15.0 cf@1.0.0-beta.23 wrangler@4.153.0 && 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` 多了這個警告

```text
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

所以這個環境變數還是要留著，[文件](https://nodejs.org/api/cli.html#node_use_env_proxy1)上它目前也還是 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 才會驗到

---

## About the author

**Ying Xiang Zhao** (geminixiang) — DevOps Engineer, Taipei, Taiwan.

Ying Xiang Zhao（geminixiang）是住在台北的 DevOps 工程師，工作橫跨雲原生基礎設施、CI/CD、ML 模型部署與全端開發。這個部落格記錄 infra、資安與 AI coding agent 的實戰筆記；作品包含開源專案 mikan（把 AI coding agent 帶進 Slack / Telegram / Discord）與 tiny-agent（用四種語言實作的極簡 coding agent），瀏覽器內的影片編輯器 Studio，並經營旅遊分帳工具 SentriPay。

**Open to work** — DevOps Engineer / Platform Engineer / Site Reliability Engineer / AI Infrastructure Engineer · Taipei, Taiwan / Remote. Résumé: https://geminixiang.com/about/ (JSON: https://geminixiang.com/resume.json)

Topics: DevOps, Cloud Native, Kubernetes, Docker, Terraform, Google Cloud, CI/CD, Security, AI coding agents, ML model deployment, TypeScript, Python

Projects:
- [mikan](https://github.com/geminixiang/mikan) — Self-hosted AI coding agent for Slack, Telegram, Discord and GitHub with per-conversation sandboxes
- [tiny-agent](https://github.com/geminixiang/tiny-agent) — Minimal AI coding agents in TypeScript, Go, Python and Rust, plus a book on how they work
- [SentriPay](https://sentripay.com) — Travel expense splitter with multi-currency conversion and shareable, live-synced trip ledgers
- [Studio](https://studio.geminixiang.com) — Browser-based video editor built on WebCodecs

[About](https://geminixiang.com/about/) · [Résumé](https://geminixiang.com/about/) · [LinkedIn](https://linkedin.com/in/geminixiang/) · [GitHub](https://github.com/geminixiang) · [Twitter](https://x.com/geminixiang)
