🐙
【2024年】フロントエンド設定集(ESLint、Prettier、EditorConfig、tsconfig.json……)
‘You gave me hyacinths first a year ago;
‘They called me the hyacinth girl.’The Waste Land By T. S. Eliot
個人開発するときに、ESLint の設定など、毎回、見直しているので、結構時間がかかっている。
時間短縮のために、最近、Remix のチュートリアルを写経したときに使った設定を Zenn の記事にメモしておこうと思う。
設定は、厳しめにしている。
理由は、個人で作っていても、一定の質を担保するため。
ESLint
.eslintrc.cjs
.eslintrc.cjs
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
},
env: {
browser: true,
commonjs: true,
es6: true,
},
ignorePatterns: ["!**/.server", "!**/.client"],
// Base config
extends: ["eslint:recommended", "prettier"],
overrides: [
// React
{
files: ["**/*.{js,jsx,ts,tsx}"],
plugins: ["react", "jsx-a11y"],
extends: [
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended",
],
settings: {
react: {
version: "detect",
},
formComponents: ["Form"],
linkComponents: [
{ name: "Link", linkAttribute: "to" },
{ name: "NavLink", linkAttribute: "to" },
],
"import/resolver": {
typescript: {},
},
},
},
// Typescript
{
files: ["*.cts", "*.ctsx", "*.mts", "*.mtsx", "*.ts", "*.tsx"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/strict-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked",
"plugin:import/typescript",
"prettier",
],
parser: "@typescript-eslint/parser",
settings: {
"import/internal-regex": "^~/",
"import/resolver": {
node: {
extensions: [".ts", ".tsx"],
},
typescript: {
alwaysTryTypes: true,
},
},
},
parserOptions: {
// sourceType: "module",
project: ["./tsconfig.json"],
},
plugins: ["@typescript-eslint", "import"],
rules: {
// ESlint core
curly: "error",
"default-case-last": "error",
eqeqeq: "error",
"no-console": "error",
"no-else-return": ["error", { allowElseIf: false }],
"no-lonely-if": "error",
"no-multi-assign": "error",
"no-negated-condition": "error",
"no-new": "error",
"no-new-object": "error",
"no-new-wrappers": "error",
"no-param-reassign": "error",
"no-return-assign": "error",
"no-self-compare": "error",
"no-sequences": ["error", { allowInParentheses: false }],
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": "error",
"no-useless-backreference": "error",
"no-useless-concat": "error",
"no-useless-rename": "error",
"no-useless-return": "error",
"object-shorthand": "error",
"one-var": ["error", "never"],
"prefer-object-spread": "error",
"sort-imports": [
"error",
{
ignoreCase: true,
ignoreDeclarationSort: true,
},
],
// @typescript-eslint
"@typescript-eslint/array-type": ["error", { default: "array-simple" }],
"@typescript-eslint/class-literal-property-style": "off",
"@typescript-eslint/consistent-indexed-object-style": "off",
"@typescript-eslint/consistent-type-assertions": [
"error",
{
assertionStyle: "as",
objectLiteralTypeAssertions: "never",
},
],
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/explicit-function-return-type": "off",
// "@typescript-eslint/explicit-function-return-type": [
// "error",
// {
// allowExpressions: true,
// allowTypedFunctionExpressions: true,
// allowHigherOrderFunctions: true,
// allowConciseArrowFunctionExpressionsStartingWithVoid: true,
// },
// ],
"@typescript-eslint/explicit-member-accessibility": "error",
"@typescript-eslint/naming-convention": [
"error",
{
selector: "accessor",
format: ["camelCase"],
},
{
selector: "enumMember",
format: ["PascalCase"],
},
{
selector: "function",
format: ["camelCase", "PascalCase"],
},
{
selector: "method",
format: ["camelCase", "PascalCase"],
filter: {
regex: "^__resolveType$",
match: false,
},
},
{
selector: "objectLiteralMethod",
format: null,
},
{
selector: "objectLiteralProperty",
format: null,
},
{
selector: "parameter",
format: ["camelCase", "PascalCase"],
leadingUnderscore: "allow",
},
{
selector: "parameter",
modifiers: ["destructured"],
format: null,
},
{
selector: "parameterProperty",
format: ["camelCase"],
},
{
selector: "typeLike",
format: ["PascalCase"],
},
{
selector: "variable",
format: ["camelCase", "PascalCase", "UPPER_CASE"],
filter: {
regex: "^_$",
match: false,
},
},
{
selector: "variable",
modifiers: ["destructured"],
format: null,
},
],
"@typescript-eslint/no-confusing-void-expression": "error",
"@typescript-eslint/no-empty-interface": [
"error",
{ allowSingleExtends: true },
],
"@typescript-eslint/no-floating-promises": [
"error",
{ ignoreVoid: true },
],
"@typescript-eslint/no-invalid-void-type": [
"error",
{ allowAsThisParameter: true },
],
"@typescript-eslint/no-misused-promises": [
"error",
{ checksVoidReturn: false },
],
"@typescript-eslint/no-namespace": [
"error",
{ allowDeclarations: true },
],
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/no-throw-literal": "error",
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "error",
"@typescript-eslint/no-unnecessary-condition": [
"error",
{ allowConstantLoopConditions: true },
],
"@typescript-eslint/no-unused-expressions": [
"error",
{ enforceForJSX: true },
],
"@typescript-eslint/no-unused-vars": "off", // Let TypeScript check it
"@typescript-eslint/no-use-before-define": [
"error",
{ functions: false },
],
"@typescript-eslint/no-useless-constructor": "error",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-readonly": "error",
"@typescript-eslint/require-array-sort-compare": "error",
"@typescript-eslint/require-await": "off",
"@typescript-eslint/restrict-template-expressions": [
"error",
{ allowNumber: true },
],
"@typescript-eslint/return-await": ["error", "always"],
"@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/strict-boolean-expressions": "error",
"@typescript-eslint/unbound-method": ["error", { ignoreStatic: true }],
// import
"import/first": "error",
"import/no-absolute-path": "error",
"import/no-default-export": "error",
"import/no-deprecated": "error",
"import/no-duplicates": "error",
"import/no-extraneous-dependencies": "error",
"import/no-mutable-exports": "error",
"import/no-relative-packages": "error",
"import/no-unassigned-import": "error",
"import/no-useless-path-segments": ["error"],
"import/order": [
"error",
{
alphabetize: { caseInsensitive: true, order: "asc" },
groups: [["builtin", "external"], "parent", ["sibling", "index"]],
"newlines-between": "always",
},
],
// Preview 202407
"@typescript-eslint/consistent-type-exports": [
"warn",
{
fixMixedExportsWithInlineTypeSpecifier: false,
},
],
"@typescript-eslint/consistent-type-imports": [
"warn"
{
fixStyle: "separate-type-imports",
prefer: "type-imports",
},
],
"@typescript-eslint/no-array-delete": "warn",
"@typescript-eslint/no-dynamic-delete": "warn",
"@typescript-eslint/no-extraneous-class": "warn",
"@typescript-eslint/no-meaningless-void-operator": "warn",
"@typescript-eslint/no-mixed-enums": "warn",
"@typescript-eslint/no-non-null-asserted-nullish-coalescing": "warn",
"@typescript-eslint/no-unnecessary-template-expression": "warn",
"import/consistent-type-specifier-style": ["warn", "prefer-top-level"],
},
},
// Node
{
files: [".eslintrc.js"],
env: {
node: true,
},
},
],
};
Remix のテンプレートで、自動で出力された、.eslintrc.cjs がベース。
TypeScript の設定は、@herp-inc/eslint-config の設定を足して、カスタマイズして使っている。
作成した当時(2024 年 7 月)、plugin:import/typescript が、ESLint の flat config に未対応だったため .cjs でクラシックな ESLint の記法で書いている。
Prettier
.prettierrc.yml
printWidth: 80
tabWidth: 2
singleQuote: false
trailingComma: "all"
semi: true
useTabs: false
.editorconfig
[*.{ts,tsx,json,js,mjs,cjs}]
end_of_line = crlf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 2
主に、VS Code の拡張機能の indent-rainbow を正常に動作させるために、設定している。
tsconfig.json(remix 用)
tsconfig.json
tsconfig.json
{
"extends": "@tsconfig/strictest/tsconfig.json",
"include": [
"**/*.ts",
"**/*.tsx",
"**/.server/**/*.ts",
"**/.server/**/*.tsx",
"**/.client/**/*.ts",
"**/.client/**/*.tsx"
],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["@remix-run/node", "vite/client"],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"target": "ES2022",
"strict": true,
"allowJs": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"]
},
// Remix takes care of building everything in `remix build`.
"noEmit": true
}
}
@tsconfig/strictest 使用
package.json
package.json
package.json
{
"name": "remix-tutorial",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"build": "remix vite:build",
"dev": "remix vite:dev",
"lint": "eslint --fix --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"lint:format": "eslint --fix --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"start": "remix-serve build/server/index.js",
"typecheck": "tsc",
"biome": "biome format --write .",
"prettier": "prettier . --write",
"format": "bun run lint:format && bun run prettier"
},
"dependencies": {
"@remix-run/node": "^2.10.3",
"@remix-run/react": "^2.10.3",
"@remix-run/serve": "^2.10.3",
"eslint-config-prettier": "^9.1.0",
"isbot": "^4.1.0",
"match-sorter": "^6.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sort-by": "^1.2.0",
"tiny-invariant": "^1.3.1"
},
"devDependencies": {
"@biomejs/biome": "1.8.3",
"@remix-run/dev": "^2.10.3",
"@tsconfig/strictest": "2.0.5",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"@typescript-eslint/parser": "^6.13.0",
"eslint": "^8.47.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "3.3.3",
"typescript": "^5.1.6",
"vite": "^5.1.4",
"vite-tsconfig-paths": "^4.3.1"
},
"engines": {
"node": ">=20.0.0"
}
}
Lint 関連のライブラリをまとめてインストールするコマンド(bun)
bun add --development --exact \
@tsconfig/strictest \
@typescript-eslint/eslint-plugin \
@typescript-eslint/parser \
eslint \
eslint-import-resolver-typescript \
eslint-plugin-import \
eslint-plugin-jsx-a11y \
eslint-plugin-react \
eslint-plugin-react-hooks \
prettier
.gitignore(Remix 用)
.gitignore
node_modules
/.cache
/build
.env
Biome
結局 Biome は使っていない。
しかし、いろいろ調べて設定したので、メモしてしておく。
biome.json
biome.json
{
"files": {
"ignore": ["tsconfig.json"]
},
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 80,
"attributePosition": "auto"
},
"organizeImports": { "enabled": true },
"linter": { "enabled": true, "rules": { "recommended": true } },
"javascript": {
"formatter": {
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"trailingCommas": "all",
"semicolons": "always",
"arrowParentheses": "always",
"bracketSpacing": true,
"bracketSameLine": true,
"quoteStyle": "double",
"attributePosition": "auto"
}
},
"overrides": [
{
"include": ["*.json"],
"formatter": {
"indentWidth": 2
}
}
]
}
Discussion