ESLint9.*対応のため.eslintrc.jsonからFlat Configに書き換える

※ 一旦、正常に動くようにするためなので細かい設定の重複などがあるかもしれません。

背景

ESLintがv9からflat configがデフォルトになり、v10で完全に削除されるということで移行作業を行いました。 eslint.org

内容

本ブログは備忘録がメインで、基本的なエラーは'#参考'の記事を見ていただければ解決できるため割愛します。 '#参考'の記事で解決できなかったものは下部に記載しています。

コード

変更前[.eslintrc.json]

{
  "env": {
    "browser": true,
    "es2021": true
  },
  "settings": {
    "react": {
      "version": "18.2.0"
    }
  },
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended",
    "plugin:prettier/recommended"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module",
    "project": ["./tsconfig.json"]
  },
  "plugins": [
    "@typescript-eslint",
    "react",
    "unused-imports",
    "import"
  ],
  "rules": {
    "unused-imports/no-unused-imports": "error",
    "prettier/prettier": ["error", {}, { "usePrettierrc": true }],
    "react/react-in-jsx-scope": "off",
    "import/order": [
      "error",
      {
        "groups": [
          "builtin",
          "external",
          "internal",
          ["parent", "sibling"],
          "object",
          "type",
          "index"
        ],
        "newlines-between": "never",
        "pathGroupsExcludedImportTypes": ["builtin"],
        "alphabetize": { "order": "asc", "caseInsensitive": true },
        "pathGroups": [
          {
            "pattern": "react**",
            "group": "external",
            "position": "before"
          },
          {
            "pattern": "{@/components/**,@/pages/**}",
            "group": "internal",
            "position": "before"
          },
          {
            "pattern": "./**.module.css",
            "group": "index",
            "position": "after"
          }
        ]
      }
    ]
  }
}

変更後[eslint.config.js]

import eslint from "@eslint/js";
import typeScriptESLintParser from "@typescript-eslint/parser";
import eslintConfigPrettier from "eslint-config-prettier";
import pluginImport from "eslint-plugin-import";
import pluginReact from "eslint-plugin-react";
import pluginUnusedImport from "eslint-plugin-unused-imports";
import globals from "globals";
import tseslint from "typescript-eslint";

export default [
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  eslintConfigPrettier,
  {
    ignores: ["**/build/**"],
  },
  {
    files: ["**/*.ts", "**/*.tsx"],
  },
  {
    languageOptions: {
      globals: {
        ...globals.browser,
        ...globals.es2021,
      },
      ecmaVersion: "latest",
      sourceType: "module",
      parser: typeScriptESLintParser,
    },
    settings: {
      react: {
        version: "18.2.0",
      },
    },
  },
  {
    plugins: {
      react: pluginReact,
      import: pluginImport,
      "unused-imports": pluginUnusedImport,
    },
    rules: {
      "no-unused-vars": "off",
      "unused-imports/no-unused-imports": "error",
      "react/react-in-jsx-scope": "off",
      "import/order": [
        "error",
        {
          groups: [
            "builtin",
            "external",
            "internal",
            ["parent", "sibling"],
            "object",
            "type",
            "index",
          ],
          "newlines-between": "never",
          pathGroupsExcludedImportTypes: ["builtin"],
          alphabetize: { order: "asc", caseInsensitive: true },
          pathGroups: [
            {
              pattern: "react**",
              group: "external",
              position: "before",
            },
            {
              pattern: "{@/components/**,@/pages/**}",
              group: "internal",
              position: "before",
            },
            {
              pattern: "./**.module.css",
              group: "index",
              position: "after",
            },
          ],
        },
      ],
    },
  },
];

参考

ESLint v9への移行

create-react-appを使わないReactにFlat configなESLint入れる

ESLintのflat configは分割して書くといいかも #JavaScript - Qiita

ESMへの移行

こちらは参考記事にもありますが一応書いてます。 export defaultとしたかったのでpackage.json"type": "module"を追記します。 自分のプロジェクトでは影響がjestのみだったため容易に移行することができました。

vscodeの設定

$ eslintを叩くことでターミナルからlintエラーを確認はできたのですが、vscode上にエラーが表示されない問題があったため以下の対応をしました。

  1. vscodeの設定から'eslint'と検索
  2. 'eslint.experimental.useFlatConfig'にチェックを入れる

huskyとlint-stagedによるcommit時のlinterでエラー検知できない

package.jsonに記述していたコマンドにオプションをつける必要がありました。

  "lint-staged": {
    "*.{ts,tsx}": [
      "prettier src/* --write",
-      "eslint"
+      "eslint --config eslint.config.js"
    ]
  },

アップロードされた画像のリサイズをJSでおこなう

実務のフロント側で画像のリサイズをおこなう必要があったため忘備録として書くことにしました。

おおまかな実装の流れ

  1. リサイズ関数の実装
    1. Fileオブジェクトを引数としたリサイズ関数の定義
    2. FileReaderオブジェクトを作成
    3. ファイルの読み込み完了時に呼び出される関数の実装
    4. Canvas を用いて画像をリサイズ
  2. 呼び出し元の実装

実装

まずはresizeImageという関数を定義します 以降asyncは動作には必要ないですが、非同期関数だということを明示するキーワードとして記述しています。 ファイルの読み込みや画像リサイズを非同期でおこなうのでPromiseオブジェクトを作成しています。 リサイズされた画像を返り値とします。

const resizeImage = async (file: File): Promise<File> => {
  const maxWidth = 1024;
  const maxHeight = 768;

  return new Promise<File>((resolve, reject) => {
  }
}

続いて、ファイルを読み込むためのFileReaderオブジェクトを作成し、ファイルの読み込みと、それが完了した時とエラーの時の関数を割り当てています。 readAsDataURLは、データURLとしてfileをBase64エンコードします

const resizeImage = async (file: File): Promise<File> => {
  const maxWidth = 1024;
  const maxHeight = 768;

  return new Promise<File>((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (event) => {};
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
};

reader.onloadに割り当てる関数を実装します。 まずはFileReaderによって読み込んだ画像データ(Base64形式)を画像として処理するためにImgオブジェクトを生成します。 FileReader同様、完了時とエラー時の関数を割り当てます。 img.srcでImgオブジェクトに画像データを読み込ませ、その後onloadにて処理が行われます。

const resizeImage = async (file: File): Promise<File> => {
  const maxWidth = 1024;
  const maxHeight = 768;

  return new Promise<File>((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (event) => {
      const img = new Image();
      img.onload = () => {}
      img.onerror = reject;
      img.src = event.target?.result as string;
    };
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
};

Canvas を用いて画像をリサイズします。 新しいcanvas要素を作成し、アスペクト比を維持するため画像のサイズ計算をおこないます。 canvasに描画するために必要なctx(context)を取得し、drawImageで画像を描画します。 最後にcanvas.toBlobでcanvasの内容をBlobオブジェクトとして取得し、Fileオブジェクトに変換します。 resolveを使ってFileオブジェクトを返します。

const resizeImage = async (file: File): Promise<File> => {
  const maxWidth = 1024;
  const maxHeight = 768;

  return new Promise<File>((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (event) => {
      const img = new Image();
      img.onload = () => {
        const canvas = document.createElement("canvas");
        // 画像のサイズ計算
        let width = img.width;
        let height = img.height;
        if (width > height) {
          if (width > maxWidth) {
            height *= maxWidth / width;
            width = maxWidth;
          }
        } else {
          if (height > maxHeight) {
            width *= maxHeight / height;
            height = maxHeight;
          }
        }

        canvas.width = width;
        canvas.height = height;

        const ctx = canvas.getContext("2d");
        ctx?.drawImage(img, 0, 0, width, height);

        canvas.toBlob((blob) => {
          if (blob) {
            // BlobをFileオブジェクトに変換して返す
            const resizedFile = new File([blob], file.name, {
              type: file.type,
            });
            resolve(resizedFile);
          } else {
            reject(new Error("canvasからFileへの変換に失敗"));
          }
        }, file.type);
      };
      img.onerror = reject;
      img.src = event.target?.result as string;
    };
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
};

最後に呼び出し側の実装です。 複数のファイルがアップロードされるのも考慮し、Promise.allで実装をしています。 完了すると、アップロード関数にリサイズ画像を渡し完了です。

  const hoge = async (files: File[]) => {
    try {
      const resizedImages = await Promise.all(files.map((file) => resizeImage(file)));
      handleUpload(resizedImages);
    } catch {
      console.log('エラー')
    }
  };