feat(eslint): add eslint flat config

This commit is contained in:
Shintaro Jokagi 2025-05-28 13:38:11 +12:00
parent 1937be58a6
commit 01f4dac75d
No known key found for this signature in database
GPG key ID: 0DDF8FA44C9A0DA8
16 changed files with 4607 additions and 2125 deletions

28
.eslint/astro.ts Normal file
View file

@ -0,0 +1,28 @@
import type { Linter } from "eslint";
// @ts-expect-error - no types available
import jsxA11y from "eslint-plugin-jsx-a11y";
import { astroFiles } from "./shared";
export const astroConfig: Linter.Config = {
name: "eslint/astro",
files: astroFiles,
plugins: {
"jsx-a11y": jsxA11y,
},
rules: {
// Astro specific adjustments
"@typescript-eslint/no-unused-vars": "off", // Astro components can have unused props
"import/no-unresolved": "off",
"no-undef": "off", // Astro has global variables like Astro
// A11y rules for Astro
"jsx-a11y/alt-text": "error",
"jsx-a11y/anchor-has-content": "error",
"jsx-a11y/anchor-is-valid": "error",
"jsx-a11y/click-events-have-key-events": "error",
"jsx-a11y/interactive-supports-focus": "error",
"jsx-a11y/no-redundant-roles": "error",
"jsx-a11y/img-redundant-alt": "error",
"jsx-a11y/no-access-key": "error",
},
};

72
.eslint/base.ts Normal file
View file

@ -0,0 +1,72 @@
import type { Linter } from "eslint";
import { sharedFiles } from "./shared";
export const baseConfig: Linter.Config = {
name: "eslint/base",
files: sharedFiles,
rules: {
"constructor-super": "error",
"for-direction": "error",
"getter-return": "error",
"no-async-promise-executor": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-binary-expression": "error",
"no-constant-condition": "error",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-args": "error",
"no-dupe-class-members": "error",
"no-dupe-else-if": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-empty-static-block": "error",
"no-ex-assign": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-global-assign": "error",
"no-import-assign": "error",
"no-invalid-regexp": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-new-native-nonconstructor": "error",
"no-nonoctal-decimal-escape": "error",
"no-obj-calls": "error",
"no-octal": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-self-assign": "error",
"no-setter-return": "error",
"no-this-before-super": "error",
"no-undef": "error",
"no-unexpected-multiline": "error",
"no-unreachable": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unsafe-optional-chaining": "error",
"no-unused-labels": "error",
"no-unused-private-class-members": "error",
"no-useless-backreference": "error",
"no-useless-catch": "error",
"no-var": "error",
"no-with": "error",
"require-yield": "error",
"use-isnan": "error",
"valid-typeof": "error",
// Additional base rules
"no-console": ["warn", { allow: ["warn", "error"] }],
"prefer-const": "error",
"object-shorthand": "error",
"prefer-template": "error",
curly: ["error", "all"],
eqeqeq: ["error", "always"],
"no-implicit-coercion": "error",
},
};

19
.eslint/config-files.ts Normal file
View file

@ -0,0 +1,19 @@
import type { Linter } from "eslint";
import { configFiles } from "./shared";
export const configFilesConfig: Linter.Config = {
name: "eslint/config-files",
files: configFiles,
rules: {
"no-console": "off",
"@typescript-eslint/no-var-requires": "off",
"import/no-default-export": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"prefer-const": "off",
"@typescript-eslint/no-explicit-any": "off",
},
};

63
.eslint/import.ts Normal file
View file

@ -0,0 +1,63 @@
import type { Linter } from "eslint";
// @ts-expect-error - no types available
import importPlugin from "eslint-plugin-import";
import { sharedFiles } from "./shared";
export const importConfigArray: Linter.Config[] = [
{
name: "eslint/import",
files: sharedFiles,
plugins: {
import: importPlugin,
},
rules: {
...importPlugin.configs.recommended.rules,
...importPlugin.configs.typescript.rules,
"import/order": [
"error",
{
groups: [
"builtin",
"external",
"internal",
["parent", "sibling"],
"index",
"object",
"type",
],
"newlines-between": "always",
alphabetize: {
order: "asc",
caseInsensitive: true,
},
pathGroups: [
{
pattern: "@/**",
group: "internal",
position: "before",
},
],
pathGroupsExcludedImportTypes: ["builtin"],
},
],
"import/no-unresolved": "off", // TypeScript handles this
"import/no-duplicates": ["error", { "prefer-inline": true }],
"import/consistent-type-specifier-style": ["error", "prefer-inline"],
"import/first": "error",
"import/newline-after-import": "error",
"import/no-default-export": "off", // Allow default exports
},
settings: {
"import/resolver": {
typescript: {
alwaysTryTypes: true,
},
node: true,
},
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"],
},
},
},
];

35
.eslint/javascript.ts Normal file
View file

@ -0,0 +1,35 @@
import type { Linter } from "eslint";
import { javascriptFiles } from "./shared";
export const javascriptConfig: Linter.Config = {
name: "eslint/javascript",
files: javascriptFiles,
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
rules: {
"no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
"no-console": ["warn", { allow: ["warn", "error"] }],
"no-debugger": "error",
"prefer-const": "error",
"no-var": "error",
"object-shorthand": "error",
"prefer-template": "error",
curly: ["error", "all"],
eqeqeq: ["error", "always"],
"no-implicit-coercion": "error",
},
};

31
.eslint/jsx-a11y.ts Normal file
View file

@ -0,0 +1,31 @@
import type { Linter } from "eslint";
// @ts-expect-error - no types available
import jsxA11y from "eslint-plugin-jsx-a11y";
import { astroFiles, javascriptFiles, typescriptFiles } from "./shared";
export const jsxA11yConfig: Linter.Config = {
name: "eslint/jsx-a11y",
files: [
...astroFiles,
...typescriptFiles.filter((f) => f.includes("tsx")),
...javascriptFiles.filter((f) => f.includes("jsx")),
],
plugins: {
"jsx-a11y": jsxA11y,
},
rules: {
...jsxA11y.configs.recommended.rules,
// Additional a11y rules
"jsx-a11y/alt-text": "error",
"jsx-a11y/anchor-has-content": "error",
"jsx-a11y/anchor-is-valid": "error",
"jsx-a11y/click-events-have-key-events": "error",
"jsx-a11y/interactive-supports-focus": "error",
"jsx-a11y/no-redundant-roles": "error",
"jsx-a11y/img-redundant-alt": "error",
"jsx-a11y/no-access-key": "error",
"jsx-a11y/label-has-associated-control": "error",
"jsx-a11y/no-autofocus": "warn",
},
};

59
.eslint/react.ts Normal file
View file

@ -0,0 +1,59 @@
import type { Linter } from "eslint";
import react from "eslint-plugin-react";
import { javascriptFiles, typescriptFiles } from "./shared";
export const reactConfig: Linter.Config = {
name: "eslint/react",
files: [
...typescriptFiles.filter((f) => f.includes("tsx")),
...javascriptFiles.filter((f) => f.includes("jsx")),
],
plugins: {
react,
},
rules: {
...react.configs.recommended.rules,
"react/react-in-jsx-scope": "off", // Not needed in modern React/Preact
"react/prop-types": "off", // Using TypeScript
"react/jsx-uses-react": "off",
"react/jsx-uses-vars": "error",
"react/no-unescaped-entities": "off",
"react/jsx-key": "error",
"react/jsx-no-useless-fragment": "error",
"react/self-closing-comp": "error",
"react/jsx-boolean-value": ["error", "never"],
"react/jsx-curly-brace-presence": [
"error",
{ props: "never", children: "never" },
],
"react/function-component-definition": [
"error",
{
namedComponents: "arrow-function",
unnamedComponents: "arrow-function",
},
],
},
settings: {
react: {
version: "detect",
},
},
};
import reactHooks from "eslint-plugin-react-hooks";
export const reactHooksConfig: Linter.Config = {
name: "eslint/react-hooks",
files: [
...typescriptFiles.filter((f) => f.includes("tsx")),
...javascriptFiles.filter((f) => f.includes("jsx")),
],
plugins: {
"react-hooks": reactHooks,
},
rules: {
...reactHooks.configs.recommended.rules,
},
};

41
.eslint/shared.ts Normal file
View file

@ -0,0 +1,41 @@
export const sharedFiles = [
"**/*.js",
"**/*.cjs",
"**/*.mjs",
"**/*.jsx",
"**/*.ts",
"**/*.cts",
"**/*.mts",
"**/*.tsx",
"**/*.d.ts",
];
export const sharedTestFiles = [
"**/*.test.{ts,tsx,js,jsx}",
"**/*.spec.{ts,tsx,js,jsx}",
"**/tests/**/*.{ts,tsx,js,jsx}",
"**/__tests__/**/*.{ts,tsx,js,jsx}",
];
export const astroFiles = ["**/*.astro"];
export const typescriptFiles = [
"**/*.ts",
"**/*.tsx",
"**/*.mts",
"**/*.cts",
"**/*.d.ts",
];
export const javascriptFiles = ["**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs"];
export const configFiles = [
"*.config.{ts,js,mjs}",
"**/*.config.{ts,js,mjs}",
"**/vite.config.*",
"**/vitest.config.*",
"**/playwright.config.*",
"**/astro.config.*",
"**/tailwind.config.*",
"**/eslint.config.*",
];

19
.eslint/test.ts Normal file
View file

@ -0,0 +1,19 @@
import type { Linter } from "eslint";
import { sharedTestFiles } from "./shared";
export const testConfig: Linter.Config = {
name: "eslint/test",
files: sharedTestFiles,
rules: {
"no-console": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/unbound-method": "off",
"import/no-extraneous-dependencies": "off",
},
};

44
.eslint/typescript.ts Normal file
View file

@ -0,0 +1,44 @@
import type { Linter } from "eslint";
import { typescriptFiles } from "./shared";
export function createTypescriptConfig(tsConfigPath: string): Linter.Config {
return {
name: "eslint/typescript",
files: typescriptFiles,
languageOptions: {
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
project: tsConfigPath,
tsconfigRootDir: process.cwd(),
},
},
rules: {
// Basic TypeScript rules that work without type information
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/consistent-type-imports": [
"error",
{ prefer: "type-imports", fixStyle: "inline-type-imports" },
],
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
// Override base rules for TypeScript
"no-unused-vars": "off", // Handled by TypeScript
"no-undef": "off", // TypeScript handles this
},
};
}

47
.prettierignore Normal file
View file

@ -0,0 +1,47 @@
# Build outputs
dist/
build/
.astro/
# Dependencies
node_modules/
# Generated files
coverage/
.nyc_output/
# Test outputs
playwright-report/
test-results/
# Package files
package-lock.json
yarn.lock
pnpm-lock.yaml
# Config files that should maintain their format
wrangler.toml
# Binary files
*.png
*.jpg
*.jpeg
*.gif
*.svg
*.ico
*.woff
*.woff2
*.ttf
*.eot
# IDE and OS
.vscode/
.idea/
# Logs
*.log
# Static assets
public/fonts/
public/favicon.ico
public/favicon.svg

44
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,44 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "explicit"
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"astro"
],
"eslint.format.enable": false,
"prettier.requireConfig": true,
"[astro]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"files.associations": {
"*.astro": "astro"
}
}

93
eslint.config.ts Normal file
View file

@ -0,0 +1,93 @@
import { includeIgnoreFile } from "@eslint/compat";
import type { TSESLint } from "@typescript-eslint/utils";
import prettierConfig from "eslint-config-prettier";
import astro from "eslint-plugin-astro";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import tseslint from "typescript-eslint";
// Import modular configurations
import { astroConfig } from "./.eslint/astro";
import { baseConfig } from "./.eslint/base";
import { configFilesConfig } from "./.eslint/config-files";
import { importConfigArray } from "./.eslint/import";
import { javascriptConfig } from "./.eslint/javascript";
import { jsxA11yConfig } from "./.eslint/jsx-a11y";
import { reactConfig, reactHooksConfig } from "./.eslint/react";
import { testConfig } from "./.eslint/test";
import { createTypescriptConfig } from "./.eslint/typescript";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const tsConfigPath = resolve(__dirname, "./tsconfig.json");
const ignoresConfig = {
name: "eslint/ignores",
ignores: [
// Build outputs
"**/dist/**",
"**/build/**",
"**/.astro/**",
"**/node_modules/**",
// Test outputs
"**/coverage/**",
"**/playwright-report/**",
"**/test-results/**",
// Config files that don't need linting
"**/*.config.js",
"**/*.config.mjs",
"**/wrangler.toml",
// Other common ignores
"**/.next/**",
"**/.nuxt/**",
"**/.output/**",
"**/.vercel/**",
"**/.netlify/**",
"**/public/**",
"**/*.min.js",
"**/*.d.ts",
"**/CHANGELOG.md",
"**/README.md",
],
} satisfies TSESLint.FlatConfig.Config;
const config: TSESLint.FlatConfig.ConfigArray = tseslint.config(
// Include .gitignore patterns
includeIgnoreFile(resolve(__dirname, ".gitignore")),
// Core configurations
ignoresConfig,
baseConfig,
// TypeScript ecosystem
...tseslint.configs.strict,
...tseslint.configs.stylistic,
createTypescriptConfig(tsConfigPath),
// Import management
...importConfigArray,
// Framework specific
reactConfig,
reactHooksConfig,
jsxA11yConfig,
// Astro specific
...astro.configs.recommended,
astroConfig,
// Language specific
javascriptConfig,
// Special cases
testConfig,
configFilesConfig,
// Prettier integration (must be last)
prettierConfig,
);
export default config;

6043
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,8 +9,12 @@
"preview": "astro preview --port 3000",
"wrangler": "wrangler",
"astro": "astro",
"lint": "biome lint ./src",
"format": "biome format ./src",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx,.astro",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx,.astro --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"biome:lint": "biome lint ./src",
"biome:format": "biome format ./src",
"prepare": "husky",
"test": "npx vitest run",
"test:coverage": "npx vitest --coverage",
@ -33,6 +37,7 @@
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"free-astro-components": "^1.2.0",
"jiti": "^2.4.2",
"lucide-astro": "^0.460.0",
"lucide-react": "^0.475.0",
"motion": "^11.13.5",
@ -45,20 +50,44 @@
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@eslint/compat": "^1.2.9",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.27.0",
"@playwright/test": "^1.52.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/user-event": "^14.6.1",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.15.18",
"@typescript-eslint/eslint-plugin": "^8.33.0",
"@typescript-eslint/parser": "^8.33.0",
"@typescript-eslint/utils": "^8.33.0",
"@vitest/coverage-istanbul": "^3.1.3",
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.5",
"eslint-import-resolver-typescript": "^4.4.1",
"eslint-plugin-astro": "^1.3.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"husky": "^9.1.7",
"jsdom": "^26.1.0",
"lint-staged": "^15.2.7",
"prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.11",
"typescript-eslint": "^8.33.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.3",
"wrangler": "^3.114.8"
"wrangler": "^4.17.0"
},
"lint-staged": {
"src/**/*.{ts,tsx,astro,js,jsx}": ["biome check --write ."]
"src/**/*.{ts,tsx,astro,js,jsx}": [
"eslint --fix",
"prettier --write"
],
"src/**/*.{json,md,css}": [
"prettier --write"
]
}
}

57
prettier.config.js Normal file
View file

@ -0,0 +1,57 @@
/**
* @type {import('prettier').Config}
*/
export default {
// Basic formatting
printWidth: 100,
tabWidth: 2,
useTabs: false,
semi: false,
singleQuote: true,
quoteProps: "as-needed",
trailingComma: "es5",
bracketSpacing: true,
bracketSameLine: false,
arrowParens: "avoid",
// Language-specific formatting
overrides: [
{
files: "*.astro",
options: {
parser: "astro",
},
},
{
files: ["*.json", "*.jsonc"],
options: {
trailingComma: "none",
},
},
{
files: ["*.md", "*.mdx"],
options: {
printWidth: 80,
proseWrap: "always",
},
},
{
files: ["*.yml", "*.yaml"],
options: {
singleQuote: false,
},
},
],
// Plugins
plugins: [
"prettier-plugin-astro",
"prettier-plugin-tailwindcss", // Must be last
],
// Plugin-specific options
tailwindFunctions: ["clsx", "cn", "twMerge"],
// Astro-specific options
astroAllowShorthand: false,
};