Dependabot の PR が TypeScript を 6.0.3 から 7.0.2 に上げた。バージョン番号だけ見れば普通のメジャーアップデートだ。しかし Astro プロジェクトでは、それをきっかけに astro check が動かなくなることがある。

Astro / Starlight を使う公開プロジェクトの Issueを調べていて、二つの対処案が目に留まった。一つは astro check 自体を CI から外す方法。もう一つは TypeScript を 6 に戻し、チェックは残したまま、Dependabot による 7.x への自動更新をいったん止める方法だ。実際にマージされたのは後者だった。

問題は単なる peer dependency の宣言違反だけではない。TypeScript 7.0 はネイティブコンパイラへの移行で、従来の TypeScript API を内部で使う言語ツールがまだ追いついていない。Microsoft の TypeScript 7.0 リリース記事も、Astro、Vue、Svelte、MDX のような埋め込み言語ツールについては、移行が整うまで TypeScript 6 を使うよう案内している。

証拠の範囲は先に明確にしておく。固定コミットの依存関係、二つの修正 PR、元のプロジェクトの GitHub Actions を確認した。さらに Node.js の標準機能だけで 5 件の合成テストを実行し、exports によって「インストール済みなのに見つからない」と誤判定される仕組みを確かめた。元の Astro プロジェクトに対する npm install や astro check は実行していない。 後述する CI 成功は元のプロジェクトによる第三者の実行結果だ。

チェックを削除するか、対応するバージョンに戻すか

発端は 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 はクローズされ、マージされていない。

チェックを実行しなければ CI が緑色に戻ることはある。ただし、astro build の成功は、astro check が担っていた .astro テンプレートや TypeScript の型診断が成功したことを意味しない。失敗したチェックを隠すために、元々あった品質ゲートを捨てるのは別の問題を作ってしまう。

マージされた PR #10 は、チェッカーを残して Web workspace の依存バージョンを元に戻した。

- "typescript": "^7.0.2"
+ "typescript": "^6.0.3"

修正後の固定コミットを読むと、apps/web/package.json には Astro ^7.2.10、@astrojs/check ^0.9.10 が記載されている。そして同じコミットの package-lock.json では、@astrojs/[email protected] が対応する TypeScript の peer 範囲を次のように宣言している。

"peerDependencies": {
  "typescript": "^5.0.0 || ^6.0.0"
}

これはロックファイルの記述を確認した結果であり、私が npm を実行して出したエラーログではない。少なくとも、このバージョンのチェッカーは TypeScript 7 をサポート範囲に含めていない。

修復後の状態には第三者の CI 証拠もある。GitHub Actions #38103905437 は PR head 5d828e8d55c5db833f5cca17f28da3b6aba05a99 で実行され、Web job の make setup-web と make check-web、Python job が成功した。これはチェックを保持した修復後についての証拠であり、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 を返す場合があると記録されている。解析エラーをすべて「TypeScript が未インストール」と解釈すれば、ユーザーは見当違いの対応を試してしまう。

この Node.js 側の仕組みだけは自分でも確認しておきたかった。そこで Astro と実物の TypeScript 7 は入れず、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 になる。一方、明示的に公開した native-ts-synthetic/package.json は解決できる。そもそも存在しないパッケージなら、いずれも MODULE_NOT_FOUND になる。

隔離された Linux 環境の Node.js v22.16.0 で、第三者 npm パッケージを使わず node --test ./node-exports-resolution.test.cjs を実行した。以下は実際の 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 のクローズだけをもって、7.0 との完全な互換性が実現したと結論づけることはできない。

影響を受けた Astro プロジェクトの CI を戻すには

まず実際の TypeScript と @astrojs/check のバージョン、ロックファイルの peer 範囲、表示されているエラーを調べる。Dependabot がアップグレードを提案したからといって、フレームワークの言語ツールまで対応済みとは限らない。

この公開リポジトリでは npm workspace の名前が web、ディレクトリが apps/web だ。以下は PR #10 の修正内容を踏まえた、読者が自分の修復ブランチで試すための手順。私は元のプロジェクトでこれらを実行していない。

# まず使用中の依存関係を確認
npm ls typescript @astrojs/check -w web

# このケースで使われた互換バージョンへ戻す
npm install --save-dev 'typescript@^6.0.3' -w web

# ロックファイルから再インストールし、チェックとビルドを確認
npm ci
npm run check -w web
npm run build -w web

依存関係が壊れている場合、npm ls 自体が非ゼロで終了することもあり、それが診断材料になる。元のコミットを保存するか、別の修復ブランチで行い、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"]

これは npm 用の一時的な制限として設定する。関係のない依存まで更新停止にしないようにしたい。解除条件も忘れずに書いておく。Astro のチェッカーや言語サービスが新しい方式に対応し、自分の CI で確認できた時点でルールを見直す。仮の回避策を永遠の固定バージョンにしないためだ。

この事例の check-web は整形チェック、astro check、ビルドを含んでいる。修復の検証では、ロックファイルが正しい組み合わせを示すか、npm ci で再現性のあるインストールができるか、astro check が本当に実行されるか、ビルドも通るかをそれぞれ見る。.astro の型エラーを検出する既存のフィクスチャがあれば、その検出機能が失われていないか確認するとさらに確実だ。

TypeScript 7.1 があれば、すぐに 6 の固定を外せるのか

一時的に旧バージョンへ戻すことは、永続的に移行を諦めることではない。Microsoft は TypeScript 7.0 の発表で新しいプログラマティック API の開発と、@typescript/typescript6 のように旧ツールと新しい tsc を共存させる方法に触れている。ただし、tsc の実行、Astro テンプレートの解析、エディタ言語サービス、CI の型チェックは別々に動作確認する必要がある。

調査時点では、Astro #17268 はクローズされ、説明を改善する PR #18053 はマージ済み。一方、.astro を TypeScript 7 の Content Mapper へ渡す PR #17852 はオープンだった。これはその時点の記録であり、後の Astro/TypeScript リリースの状態まで保証するものではない。

TypeScript のメジャー更新で astro check が壊れたら、まずエラーと依存関係の契約を確認し、型チェックを残したまま対応する組み合わせへ戻し、ロックファイルと CI を再検証する。その後、新しい方式への移行を別件として計画すればよい。目標は単に赤い CI を消すことではなく、元々あった検査を機能させ続けることだ。

Astro を選んだ理由については、以前書いた Astro、Markdown、GitHub Actions の比較にまとめている。今回の話は、その選択をした後の自動依存更新が、知らないうちに既存の品質チェックを削ってしまわないようにするための話だ。