はじめに
自社で運営する Bitcoin 教育ライブラリ bitcoin.ne.jp で、フレームワーク・周辺ツールのメジャーバージョンを 1セッション で一気に上げました。
| 項目 | Before | After |
|---|---|---|
| Next.js | 15.5.x | 16.2.4 |
| TypeScript | 5.x | 6.0.3 |
| TailwindCSS | 3.4.x | 4.2.2 |
| ESLint | 9.x | 10.2.1 |
| eslint-config-next | 15.x | 16.2.4 |
| React / React DOM | 19.2.4 | 19.2.5 |
ハマりどころが多いので、実行順序とコマンド・パッチ・対処法をまとめた移行プレイブックです。
結論先出し: 最適な実行順序
1. TypeScript 5 → 6
2. Next.js 15 → 16 + eslint-config-next 16
3. TailwindCSS 3 → 4(公式 upgrade tool)
4. ESLint 9 → 10(patch-package で eslint-plugin-react を 2 行修正)
各段階で npm run build && npm run lint && npm test を回し、グリーンを確認してから次に進みます。途中で詰まったら戻りやすい順番にしてあります。
1. TypeScript 5 → 6
npm install --save-dev typescript@6
npx tsc --noEmit
起こるエラー
src/app/layout.tsx(4,8): error TS2882: Cannot find module or type declarations for side-effect import of './globals.css'.
TypeScript 6 はモジュール解決が厳格化され、副作用 CSS インポートに型宣言が必要 になりました。
対処
// TypeScript 6 で side-effect の CSS インポートに型宣言が必要
declare module '*.css';
declare module '*.scss';
これだけで通ります。include に **/*.ts が含まれていれば自動で拾われます。
2. Next.js 15 → 16 + eslint-config-next 16
npm install next@16 react@latest react-dom@latest
npm install --save-dev eslint-config-next@16
自動更新される tsconfig.json
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
- "jsx": "preserve",
+ "jsx": "react-jsx",
"incremental": true,
...
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
- ".next/types/**/*.ts"
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts"
],
next build 実行時に自動修正されます。手で書き戻さないこと。
eslint-config-next 16 は flat config 直接 import に対応
eslint-config-next@16 から eslint-config-next/core-web-vitals と eslint-config-next/typescript を直接インポート可能な flat config として export しています。FlatCompat ブリッジは不要になりました。
import nextCoreWebVitals from 'eslint-config-next/core-web-vitals';
import nextTypescript from 'eslint-config-next/typescript';
const eslintConfig = [
...nextCoreWebVitals,
...nextTypescript,
{
ignores: ['node_modules/**', '.next/**', 'out/**', 'public/**', 'next-env.d.ts'],
},
];
export default eslintConfig;
npm uninstall @eslint/eslintrc # FlatCompat 不要
3. TailwindCSS 3 → 4
公式 upgrade tool が用意されているので、まずは試します。
npx @tailwindcss/upgrade --force
upgrade tool が自動で行うこと
| 対象 | 変更 |
|---|---|
tailwind.config.ts |
削除 |
globals.css |
@tailwind base/components/utilities → @import 'tailwindcss'
|
globals.css |
@theme ブロックに font/color tokens 移植 |
globals.css |
@custom-variant dark で darkMode 設定を移植 |
postcss.config.mjs |
プラグイン tailwindcss → @tailwindcss/postcss
|
| 全テンプレート(*.tsx) |
text-[var(--x)] → text-(--x) ショートハンド変換 |
| 全テンプレート |
outline-none → outline-hidden, flex-shrink-0 → shrink-0
|
| 全テンプレート |
rounded → rounded-sm, rounded-sm → rounded-xs
|
bitcoin.ne.jp では 21 ファイルが自動書き換え され、それでビルドが通りました。
手動確認ポイント
- カスタム
@layer componentsを使っていれば、Tailwind 4 で動くか個別検証 -
darkMode: 'class'を使っていた場合 →@custom-variant dark (&:where(.dark, .dark *))形式へ - ボーダーのデフォルト色が
currentcolorに変わったので、borderを使っているところは色指定が必要なケースあり(upgrade tool が compatibility CSS を入れてくれる)
4. ESLint 9 → 10(最大の罠)
npm install --save-dev eslint@10
npm run lint
起こるエラー
TypeError: Error while loading rule 'react/display-name':
contextOrFilename.getFilename is not a function
at resolveBasedir (.../eslint-plugin-react/lib/util/version.js:31:100)
ESLint 10 で context.getFilename() が削除されました(context.filename プロパティ移行)。
しかし eslint-config-next@16 がバンドルする eslint-plugin-react@7.37.5 は まだ ESLint 10 非対応です。peerDependencies は "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" で 10 が抜けています。
対処: patch-package で 2 行修正
npm install --save-dev patch-package
node_modules/eslint-config-next/node_modules/eslint-plugin-react/lib/util/version.js の 31 行目:
function resolveBasedir(contextOrFilename) {
if (contextOrFilename) {
- const filename = typeof contextOrFilename === 'string' ? contextOrFilename : contextOrFilename.getFilename();
+ const filename = typeof contextOrFilename === 'string' ? contextOrFilename : (contextOrFilename.filename || contextOrFilename.getFilename());
同じく lib/rules/jsx-filename-extension.js の 64 行目:
create(context) {
- const filename = context.getFilename();
+ const filename = context.filename || context.getFilename();
両方とも .filename を優先しつつ古い API にフォールバックします。
パッチを永続化
npx patch-package eslint-config-next/eslint-plugin-react
これで patches/eslint-config-next++eslint-plugin-react+7.37.5.patch が生成されます。
package.json:
{
"scripts": {
"postinstall": "patch-package"
}
}
npm install 時に毎回自動適用されます。
別のマシンや CI でも動くか
bitcoin.ne.jp の場合は Cloudflare Pages のビルド環境でも postinstall が走るため、追加設定なしで動きました。
各段階の React-Hooks ルール対応
eslint-plugin-react-hooks が更新され、新ルール react-hooks/set-state-in-effect が追加されました。
// LangProvider など、cookie/localStorage からの hydration で setState を effect 内で呼ぶケース
useEffect(() => {
const clientLocale = getClientLocale();
if (clientLocale !== locale) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setLocaleState(clientLocale);
}
}, []);
意図的なケース(client-only ソースからの初期化)はコメント付きで disable で OK。リファクタリングは不要です。
移行後の検証チェックリスト
| 項目 | コマンド |
|---|---|
| 型チェック | npx tsc --noEmit |
| Lint | npm run lint |
| テスト | npm test |
| ビルド | npm run build |
| ローカル確認 | npm run start |
| デザイン回帰 | 主要ページを目視(Tailwind 4 のクラス変換結果) |
bitcoin.ne.jp では 4 段階を 1 セッションで通し、リグレッションゼロでデプロイ完了しました。
まとめ
TypeScript 6 → CSS ambient declarations を1ファイル追加
Next.js 16 → tsconfig 自動更新を受け入れる、flat config 直接 import
Tailwind 4 → @tailwindcss/upgrade を信頼する
ESLint 10 → patch-package で eslint-plugin-react を2行パッチ + postinstall
この移行は自社プロダクト bitcoin.ne.jp(ビットコイン図書館) で実行・本番反映済みです。