Astro check 遇上 TypeScript 7:別刪掉型別檢查,先修好相依套件
一個 Dependabot PR 把 TypeScript 從 6.0.3 升到 7.0.2。版本號看起來只是一次正常的大版本升級,對 Astro 專案來說,astro check 卻可能因此無法正常執行。
我查到一份公開的 Astro/Starlight 專案紀錄,裡面出現兩條完全不同的修復路線。一條是把 astro check 從 CI 移除,讓失敗的檢查不再執行;另一條是回退至 TypeScript 6,保留型別檢查,並暫時阻止 Dependabot 再次升到 7.x。最後被合併的是後者。
問題不只是某個 peer dependency 還沒更新支援版本。TypeScript 7.0 改成原生編譯器,部分框架語言工具仍仰賴 TypeScript 6 的程式化 API。微軟在 TypeScript 7.0 正式發布文章中也直接建議:Astro、Vue、Svelte、MDX 等嵌入式語言工具鏈,在完成遷移前先維持 TypeScript 6。
先把證據邊界說清楚:我檢查了固定版本的相依檔案、兩份 PR 及原專案 GitHub Actions,也獨立執行了 5 個不依賴第三方套件的 Node.js 合成測試,確認 exports 如何造成「套件明明存在卻被判定未安裝」。我沒有在該專案執行 npm install 或 astro check。 下文的 Astro CI 成功是原專案的執行證據,不是我獨立重現的 Astro 實測。
兩條修復路線:移除檢查,還是恢復相容版本
這次問題記錄在 vishwakarma Issue #8。之前的 Dependabot PR #3將 TypeScript 升級到 7.0.2,維護者回報新的組合讓 astro check 遇到相容問題。
PR #9 曾經提出一種看似省事的作法:從 apps/web/package.json 移除 check 指令與 @astrojs/check 相依套件,同時從 Makefile 拿掉 typecheck-web,讓 check-web 只剩格式檢查及建置。這個 PR 最後關閉,沒有合併。
拿掉失敗的檢查,建置當然可能重新變綠燈,但那不等於型別問題已經修好。astro build 不能直接取代 astro check 對 .astro 範本和 TypeScript 程式碼的型別診斷。原本應該把關的流程,不應該因為版本升級失敗就悄悄消失。
真正合併的 PR #10選擇保留 astro check,並將 Web workspace 的 TypeScript 相依版本調回:
- "typescript": "^7.0.2"
+ "typescript": "^6.0.3"
我讀取了修復後的固定 commit。其中 apps/web/package.json 指定 Astro ^7.2.10 和 @astrojs/check ^0.9.10;同一版的 package-lock.json 則顯示 @astrojs/[email protected] 的 peer 相依範圍:
"peerDependencies": {
"typescript": "^5.0.0 || ^6.0.0"
}
這段是從鎖定檔讀到的相依套件宣告,不是我執行 npm 後截取的錯誤輸出。對這個明確版本的檢查器而言,TypeScript 7 不在宣告的支援範圍內。
修復後也有公開的執行證據:GitHub Actions #38103905437 使用 PR head 5d828e8d55c5db833f5cca17f28da3b6aba05a99。Web 工作中的 make setup-web、make check-web,以及 Python 工作皆成功完成。它支持的是保留檢查器後的修復狀態,不代表我親自驗證了 TypeScript 7 的失敗情境。
為什麼 TypeScript 明明已安裝,卻可能被誤判不存在?
這裡至少有兩個不同的相容性層次。
更根本的是編譯器 API。TypeScript 7.0 改用 Go 實作的原生編譯器。tsc 命令列程式能跑,不表示 TypeScript 6 的穩定程式化 API 也完整存在。Astro 需要透過自己的語言服務分析 .astro 檔案中的 TypeScript,光升級命令列編譯器並不能自動完成這些整合。
另一層比較容易把人帶錯方向。Astro 上游的 Issue #17268 記錄過,TypeScript 7 調整套件的 exports 後,舊程式的 require.resolve('typescript') 可能回傳 ERR_PACKAGE_PATH_NOT_EXPORTED。如果檢測程式把所有解析例外都歸類成「沒有安裝」,使用者得到的修復提示就會失真。
我不想只根據 Issue 猜這段機制,所以使用 Node.js v22.16.0 的標準函式庫,製作兩個暫時性的模擬套件:一個維持舊式可解析的入口,另一個只公開 ./package.json 子路徑,刻意不公開根入口。以下是實際測試中生成的關鍵設定,排版略作整理:
{
name: 'native-ts-synthetic',
version: '7.0.0-fixture',
type: 'module',
exports: {
'./package.json': './package.json'
}
}
對這個合成測試套件執行 require.resolve('native-ts-synthetic'),會得到 ERR_PACKAGE_PATH_NOT_EXPORTED,但檔案確實存在。由於它特別公開 ./package.json,查詢 native-ts-synthetic/package.json 就能確認套件存在;真的不存在的套件則是 MODULE_NOT_FOUND。
我在隔離 Linux 環境,以 Node.js v22.16.0 執行 node --test ./node-exports-resolution.test.cjs,沒有下載 npm 第三方套件。以下是真實 Node TAP 測試日誌的節錄,不是 Astro 錯誤日誌:
# OBSERVED: legacy package resolution = FOUND
# OBSERVED: package exists; require.resolve(root) = ERR_PACKAGE_PATH_NOT_EXPORTED
# OBSERVED: require.resolve(package.json) = FOUND
# OBSERVED: absent package = MODULE_NOT_FOUND in both paths
# OBSERVED: catch-all = false missing; code-aware fallback = detected installed
# tests 5
# pass 5
# fail 0
5 個測試全部通過,證明的是特定 Node.js 套件解析機制,並不代表已執行真正的 TypeScript 7、Astro 或 astro check。package.json 的回退方法也不是萬用解:如果實際套件沒有公開那個子路徑,同樣可能解析失敗。
就算修好了「是否已安裝」的判斷,也不能憑空補上 TypeScript 7.0 尚未提供的舊 API。Astro 的 PR #18053 在 2026 年 9 月合併,改進了面對 TypeScript 7 時的錯誤說明;相關 Issue 結案不能解讀成 TypeScript 7.0 已完整支援 Astro 的型別檢查。
真的碰上這個問題,怎麼恢復 CI?
對版本組合相近的 Astro 專案,我會先看目前 TypeScript、@astrojs/check 的實際版本、鎖定檔 peer 範圍,以及原始的檢查錯誤。Dependabot 提議升級,不代表整條語言工具鏈都相容。
這個範例採用 npm workspace,名字是 web,對應 apps/web。下列指令是讀者可以在自己的修復分支執行的步驟,依照 PR #10 的公開修改整理;我沒有在原專案執行這組指令:
# 在專案根目錄確認目前的版本
npm ls typescript @astrojs/check -w web
# 將此案例回復為 TypeScript 6,並更新鎖定檔
npm install --save-dev 'typescript@^6.0.3' -w web
# 按鎖定檔重裝,保留型別檢查與建置
npm ci
npm run check -w web
npm run build -w web
發生相依衝突時,npm ls 本身就可能以非零狀態結束,這也是有用的診斷線索。執行前請保留原始 commit,或先建立修復分支,並確認 Node/npm 的版本需求及真正的 workspace 名稱。一般單套件專案不需要 -w web。^6.0.3 只是這個固定案例的解法,不是所有 Astro 專案都應該永遠固定的版本。
接著,避免自動升級 PR 不斷把同一個問題帶回來。範例中固定版的 .github/dependabot.yml 在 npm 設定內加入:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
ignore:
- dependency-name: typescript
versions: [">=7.0.0"]
這條 ignore 應放在對應的 npm 更新項目底下,不要順便封鎖其他無關的相依套件。還要設定未來重新檢查的條件:等 Astro 的檢查器和語言服務真正支援新整合方式,並由自己的 CI 通過後,再移除規則。否則一個暫時性方案很容易變成永久鎖版。
原範例的 check-web 會執行格式檢查、astro check 和建置。因此,正確驗收至少包括:鎖定檔使用期望的版本、npm ci 能重建依賴、astro check 真的成功完成、建置也能通過。如果原本已有會觸發 .astro 型別錯誤的測試範例,也應確認檢查器仍能抓到它;不要只看整體 CI 顯示綠色。
TypeScript 7.1 以後,是否還要留在 6?
暫時降版不需要成為永久方案。微軟在 TypeScript 7.0 公布時已提到新的程式化 API 工作,以及 @typescript/typescript6 等讓舊工具與新 tsc 共存的選擇。但「命令列編譯器可用」、「Astro 範本能分析」、「編輯器語言服務正常」與「CI 型別檢查通過」,是不同的驗收項目。
在這次資料查核時,Astro #17268 已結案、錯誤說明改進的 PR #18053 已合併,而將 .astro 交給 TypeScript 7 Content Mapper 的 PR #17852 仍是開放狀態。這只是特定時間點的專案狀態,未來版本發布後需要重新查證。
遇到 astro check 被大版本升級打壞,較務實的順序是:先確認錯誤和相依契約,保留檢查器,回到相容組合,驗證鎖定檔與 CI,然後再安排下一次遷移。目標不是讓紅色流水線安靜,而是讓原本的型別檢查繼續有效。
如果你還在考慮個人技術部落格該用哪套架構,可以參考我之前比較 Astro、Markdown 與 GitHub Actions 的文章;這次要處理的則是選好 Astro 之後,下一次自動依賴更新會不會悄悄拿掉原本的品質把關。