diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..cc7a828 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,208 @@ +import js from '@eslint/js'; +import typescript from '@typescript-eslint/eslint-plugin'; +import typescriptParser 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 jsxA11y from 'eslint-plugin-jsx-a11y'; + +export default [ + js.configs.recommended, + { + files: ['**/*.{js,jsx,ts,tsx}'], + languageOptions: { + parser: typescriptParser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + }, + plugins: { + '@typescript-eslint': typescript, + 'react': react, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + 'jsx-a11y': jsxA11y, + }, + rules: { + // TypeScript rules + ...typescript.configs.recommended.rules, + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-non-null-assertion': 'warn', + + // React rules + ...react.configs.recommended.rules, + 'react/react-in-jsx-scope': 'off', // Not needed with React 17+ + 'react/prop-types': 'off', // Using TypeScript instead + 'react/jsx-uses-react': 'off', // Not needed with React 17+ + 'react/jsx-uses-vars': 'error', + 'react/jsx-no-undef': 'error', + 'react/jsx-key': 'error', + 'react/jsx-no-duplicate-props': 'error', + 'react/jsx-pascal-case': 'error', + 'react/no-array-index-key': 'warn', + 'react/no-danger': 'warn', + 'react/no-deprecated': 'error', + 'react/no-direct-mutation-state': 'error', + 'react/no-unescaped-entities': 'error', + 'react/no-unknown-property': 'error', + 'react/require-render-return': 'error', + 'react/self-closing-comp': 'error', + 'react/jsx-wrap-multilines': 'error', + 'react/jsx-closing-bracket-location': 'error', + 'react/jsx-closing-tag-location': 'error', + 'react/jsx-curly-spacing': ['error', 'never'], + 'react/jsx-equals-spacing': ['error', 'never'], + 'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'], + 'react/jsx-max-props-per-line': ['error', { maximum: 1, when: 'multiline' }], + 'react/jsx-no-bind': 'warn', + 'react/jsx-no-literals': 'off', + 'react/jsx-sort-props': 'off', + + // React Hooks rules + ...reactHooks.configs.recommended.rules, + + // React Refresh rules + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + + // Accessibility rules + ...jsxA11y.configs.recommended.rules, + 'jsx-a11y/alt-text': 'error', + 'jsx-a11y/anchor-has-content': 'error', + 'jsx-a11y/anchor-is-valid': 'error', + 'jsx-a11y/aria-props': 'error', + 'jsx-a11y/aria-proptypes': 'error', + 'jsx-a11y/aria-unsupported-elements': 'error', + 'jsx-a11y/click-events-have-key-events': 'warn', + 'jsx-a11y/heading-has-content': 'error', + 'jsx-a11y/img-redundant-alt': 'error', + 'jsx-a11y/no-access-key': 'error', + 'jsx-a11y/role-has-required-aria-props': 'error', + 'jsx-a11y/role-supports-aria-props': 'error', + 'jsx-a11y/scope': 'error', + 'jsx-a11y/tabindex-no-positive': 'error', + + // General JavaScript/TypeScript rules + 'no-console': 'warn', + 'no-debugger': 'error', + 'no-unused-vars': 'off', // Handled by TypeScript version + 'prefer-const': 'error', + 'no-var': 'error', + 'eqeqeq': ['error', 'always'], + 'curly': 'warn', // Changed from error to warn + 'brace-style': ['error', '1tbs'], + 'comma-dangle': 'warn', // Changed from error to warn + 'comma-spacing': ['error', { before: false, after: true }], + 'comma-style': ['error', 'last'], + 'computed-property-spacing': ['error', 'never'], + 'func-call-spacing': ['error', 'never'], + 'key-spacing': ['error', { beforeColon: false, afterColon: true }], + 'keyword-spacing': ['error', { before: true, after: true }], + 'object-curly-spacing': ['error', 'always'], + 'semi-spacing': ['error', { before: false, after: true }], + 'space-before-blocks': 'error', + 'space-before-function-paren': ['error', 'never'], + 'space-in-parens': ['error', 'never'], + 'space-infix-ops': 'error', + 'space-unary-ops': ['error', { words: true, nonwords: false }], + 'quotes': 'warn', // Changed from error to warn + 'max-len': ['warn', { code: 120, ignoreUrls: true, ignoreStrings: true }], + }, + settings: { + react: { + version: 'detect', + }, + }, + }, + { + files: ['**/*.js'], + languageOptions: { + globals: { + console: 'readonly', + process: 'readonly', + Buffer: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + global: 'readonly', + module: 'readonly', + require: 'readonly', + exports: 'readonly', + }, + }, + }, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + globals: { + // Node.js globals + console: 'readonly', + process: 'readonly', + Buffer: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + global: 'readonly', + module: 'readonly', + require: 'readonly', + exports: 'readonly', + // Browser globals + window: 'readonly', + document: 'readonly', + navigator: 'readonly', + localStorage: 'readonly', + sessionStorage: 'readonly', + fetch: 'readonly', + setTimeout: 'readonly', + clearTimeout: 'readonly', + setInterval: 'readonly', + clearInterval: 'readonly', + URL: 'readonly', + URLSearchParams: 'readonly', + FormData: 'readonly', + File: 'readonly', + FileReader: 'readonly', + Blob: 'readonly', + crypto: 'readonly', + TextEncoder: 'readonly', + TextDecoder: 'readonly', + CryptoKey: 'readonly', + btoa: 'readonly', + atob: 'readonly', + Element: 'readonly', + HTMLElement: 'readonly', + HTMLImageElement: 'readonly', + HTMLInputElement: 'readonly', + HTMLFormElement: 'readonly', + HTMLTextAreaElement: 'readonly', + HTMLSelectElement: 'readonly', + HTMLButtonElement: 'readonly', + HTMLDivElement: 'readonly', + HTMLSpanElement: 'readonly', + HTMLAnchorElement: 'readonly', + HTMLImageElement: 'readonly', + NodeJS: 'readonly', + React: 'readonly', + }, + }, + }, + { + ignores: [ + 'node_modules/**', + 'dist/**', + 'build/**', + 'out/**', + '*.min.js', + 'coverage/**', + '.nyc_output/**', + 'backend/**', + 'deployment/**', + 'web-calls/**', + ], + }, + ]; diff --git a/package.json b/package.json index c52d0e9..2a136a0 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "backend:clean": "rm -rf backend/data", "frontend:dev": "vite frontend", "frontend:typecheck": "tsc --project frontend", - "frontend:build": "npm run frontend:typecheck && vite build frontend", + "frontend:lint": "eslint frontend/src --ext .js,.jsx,.ts,.tsx", + "frontend:lint:fix": "eslint frontend/src --ext .js,.jsx,.ts,.tsx --fix", + "frontend:build": "npm run frontend:lint && npm run frontend:typecheck && vite build frontend", "frontend:electron:dev": "VITE_ELECTRON=true npm run frontend:dev", "frontend:electron:build": "VITE_ELECTRON=true npm run frontend:build && rm -rf out && electron-forge make --force --arch arm64,x64", "frontend:preview": "vite preview frontend", @@ -44,13 +46,21 @@ "@electron-forge/plugin-auto-unpack-natives": "^7.9.0", "@electron-forge/plugin-fuses": "^7.9.0", "@electron/fuses": "^1.0.0", + "@eslint/js": "^9.37.0", "@types/react": "^19.1.13", "@types/react-dom": "^19.1.9", + "@typescript-eslint/eslint-plugin": "^8.46.0", + "@typescript-eslint/parser": "^8.46.0", "@vitejs/plugin-react": "^5.0.3", "autoprefixer": "^10.4.21", "concurrently": "^9.2.1", "dotenv-cli": "^10.0.0", "electron": "^38.1.2", + "eslint": "^9.37.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.0", + "eslint-plugin-react-refresh": "^0.4.23", "husky": "^9.1.7", "postcss": "^8.5.6", "rollup-plugin-visualizer": "^6.0.4",