1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js 15→16 / TypeScript 5→6 / Tailwind 3→4 / ESLint 9→10 を1セッションで上げた手順

1
Posted at

はじめに

自社で運営する 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 インポートに型宣言が必要 になりました。

対処

src/types.d.ts
// 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-vitalseslint-config-next/typescript直接インポート可能な flat config として export しています。FlatCompat ブリッジは不要になりました。

eslint.config.mjs
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 darkdarkMode 設定を移植
postcss.config.mjs プラグイン tailwindcss@tailwindcss/postcss
全テンプレート(*.tsx) text-[var(--x)]text-(--x) ショートハンド変換
全テンプレート outline-noneoutline-hidden, flex-shrink-0shrink-0
全テンプレート roundedrounded-sm, rounded-smrounded-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:

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(ビットコイン図書館) で実行・本番反映済みです。


bitcoin.ne.jp(ビットコイン図書館)

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?