From a89cd722c9302bc55a329ce11fe9b0fdf5ffb117 Mon Sep 17 00:00:00 2001 From: Ember <197652334@qq.com> Date: Wed, 5 Nov 2025 11:05:07 +0800 Subject: [PATCH] test: add eslint and prettier configuration with pre-commit hook --- .husky/_/husky.sh | 36 ++++++++++++++++++++ .husky/pre-commit | 4 +++ web/.prettierignore | 22 ++++++++++++ web/.prettierrc.json | 13 +++++++ web/eslint.config.js | 81 ++++++++++++++++++++++++++++++++++++++++++++ web/package.json | 28 ++++++++++++++- 6 files changed, 183 insertions(+), 1 deletion(-) create mode 100755 .husky/_/husky.sh create mode 100755 .husky/pre-commit create mode 100644 web/.prettierignore create mode 100644 web/.prettierrc.json create mode 100644 web/eslint.config.js diff --git a/.husky/_/husky.sh b/.husky/_/husky.sh new file mode 100755 index 00000000..cec959a6 --- /dev/null +++ b/.husky/_/husky.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env sh +if [ -z "$husky_skip_init" ]; then + debug () { + if [ "$HUSKY_DEBUG" = "1" ]; then + echo "husky (debug) - $1" + fi + } + + readonly hook_name="$(basename -- "$0")" + debug "starting $hook_name..." + + if [ "$HUSKY" = "0" ]; then + debug "HUSKY env variable is set to 0, skipping hook" + exit 0 + fi + + if [ -f ~/.huskyrc ]; then + debug "sourcing ~/.huskyrc" + . ~/.huskyrc + fi + + readonly husky_skip_init=1 + export husky_skip_init + sh -e "$0" "$@" + exitCode="$?" + + if [ $exitCode != 0 ]; then + echo "husky - $hook_name hook exited with code $exitCode (error)" + fi + + if [ $exitCode = 127 ]; then + echo "husky - command not found in PATH=$PATH" + fi + + exit $exitCode +fi diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..25b3e6b7 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +cd web && npx lint-staged diff --git a/web/.prettierignore b/web/.prettierignore new file mode 100644 index 00000000..2ca5a7fe --- /dev/null +++ b/web/.prettierignore @@ -0,0 +1,22 @@ +# Dependencies +node_modules + +# Build outputs +dist +build +*.tsbuildinfo + +# Config files +pnpm-lock.yaml +package-lock.json +yarn.lock + +# Logs +*.log + +# Coverage +coverage + +# IDE +.vscode +.idea diff --git a/web/.prettierrc.json b/web/.prettierrc.json new file mode 100644 index 00000000..1f384ff0 --- /dev/null +++ b/web/.prettierrc.json @@ -0,0 +1,13 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "endOfLine": "lf", + "arrowParens": "always", + "bracketSpacing": true, + "jsxSingleQuote": false, + "quoteProps": "as-needed" +} diff --git a/web/eslint.config.js b/web/eslint.config.js new file mode 100644 index 00000000..057e0b94 --- /dev/null +++ b/web/eslint.config.js @@ -0,0 +1,81 @@ +import js from '@eslint/js' +import tseslint from '@typescript-eslint/eslint-plugin' +import tsparser from '@typescript-eslint/parser' +import react from 'eslint-plugin-react' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import prettier from 'eslint-plugin-prettier' + +export default [ + { + ignores: ['dist', 'node_modules', 'build', '*.config.js'] + }, + js.configs.recommended, + { + files: ['**/*.{ts,tsx}'], + languageOptions: { + parser: tsparser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true + } + }, + globals: { + window: 'readonly', + document: 'readonly', + console: 'readonly', + setTimeout: 'readonly', + clearTimeout: 'readonly', + setInterval: 'readonly', + clearInterval: 'readonly', + fetch: 'readonly', + localStorage: 'readonly', + sessionStorage: 'readonly' + } + }, + plugins: { + '@typescript-eslint': tseslint, + 'react': react, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + 'prettier': prettier + }, + rules: { + ...tseslint.configs.recommended.rules, + ...react.configs.recommended.rules, + ...reactHooks.configs.recommended.rules, + + // Prettier integration + 'prettier/prettier': 'error', + + // React rules + 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 'off', + + // TypeScript rules + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-unused-vars': ['warn', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_' + }], + + // React Refresh + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true } + ], + + // General rules + 'no-console': ['warn', { allow: ['warn', 'error'] }], + 'no-debugger': 'warn' + }, + settings: { + react: { + version: 'detect' + } + } + } +] diff --git a/web/package.json b/web/package.json index ed1c0732..f8c4655a 100644 --- a/web/package.json +++ b/web/package.json @@ -5,7 +5,12 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint:fix": "eslint . --ext ts,tsx --fix", + "format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx,css,json}\"", + "prepare": "husky" }, "dependencies": { "@radix-ui/react-slot": "^1.2.3", @@ -22,13 +27,34 @@ "zustand": "^5.0.2" }, "devDependencies": { + "@eslint/js": "^9.39.1", "@types/react": "^18.3.17", "@types/react-dom": "^18.3.5", + "@typescript-eslint/eslint-plugin": "^8.46.3", + "@typescript-eslint/parser": "^8.46.3", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "husky": "^9.1.7", + "lint-staged": "^16.2.6", "postcss": "^8.4.49", + "prettier": "^3.6.2", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", "vite": "^6.0.7" + }, + "lint-staged": { + "*.{ts,tsx}": [ + "eslint --fix", + "prettier --write" + ], + "*.{css,json}": [ + "prettier --write" + ] } }