import { defineConfig, globalIgnores } from 'eslint/config'; import js from '@eslint/js'; import tseslint from 'typescript-eslint'; import pluginReact from 'eslint-plugin-react'; import pluginReactHooks from 'eslint-plugin-react-hooks'; import pluginPromise from 'eslint-plugin-promise'; import pluginJsxA11y from 'eslint-plugin-jsx-a11y'; import pluginImport from 'eslint-plugin-import'; import pluginRegex from 'eslint-plugin-regex'; import pluginVitest from '@vitest/eslint-plugin'; import pluginStorybook from 'eslint-plugin-storybook'; import pluginComments from '@eslint-community/eslint-plugin-eslint-comments'; import globals from 'globals'; import prettier from 'eslint-config-prettier'; export default defineConfig([ globalIgnores(['**/node_modules/', '**/dist/', '**/test/', '.storybook/public/', 'coverage', 'app/react/portainer/generated-api/']), js.configs.recommended, { plugins: { '@eslint-community/eslint-comments': pluginComments }, rules: pluginComments.configs.recommended.rules, }, ...pluginStorybook.configs['flat/recommended'], { languageOptions: { globals: { ...globals.browser, ...globals.jquery, ...globals.node, angular: 'readonly', }, ecmaVersion: 'latest', sourceType: 'module', }, rules: { 'no-console': 'error', 'no-alert': 'error', 'no-control-regex': 'error', 'no-empty': 'warn', 'no-empty-function': 'warn', 'no-useless-escape': 'off', // Preserve ESLint 8 default: catch-block error variables are not checked 'no-unused-vars': ['error', { caughtErrors: 'none' }], }, }, { files: ['app/**/*.ts', 'app/**/*.tsx'], extends: [ ...tseslint.configs.recommended, pluginReact.configs.flat.recommended, pluginReact.configs.flat['jsx-runtime'], pluginReactHooks.configs.flat['recommended-latest'], pluginJsxA11y.flatConfigs.recommended, pluginImport.flatConfigs.typescript, // @ts-expect-error - pluginPromise don't have types so it errors pluginPromise.configs['flat/recommended'], ], plugins: { regex: pluginRegex, }, languageOptions: { ecmaVersion: 'latest', sourceType: 'module', parserOptions: { project: './tsconfig.json', }, }, settings: { react: { version: 'detect' }, 'import/resolver': { alias: { map: [ ['@@', './app/react/components'], ['@', './app'], ], extensions: ['.js', '.ts', '.tsx'], }, typescript: true, node: true, }, }, rules: { 'no-console': 'error', 'no-use-before-define': 'off', 'no-shadow': 'off', 'no-plusplus': 'off', 'no-underscore-dangle': 'off', 'no-await-in-loop': 'off', 'consistent-return': 'off', 'default-case': 'off', // covered by @typescript-eslint/switch-exhaustiveness-check 'func-style': ['error', 'declaration'], 'no-continue': 'error', 'no-template-curly-in-string': 'warn', // Allow named function expressions where `this` binding is needed (e.g. Yup .test()) 'prefer-arrow-callback': ['error', { allowNamedFunctions: true }], 'func-names': 'warn', 'no-param-reassign': [ 'error', { props: true, ignorePropertyModificationsFor: ['acc', 'accumulator'], }, ], 'no-restricted-exports': ['error', { restrictedNamedExports: ['default', 'then'] }], // Secure-context-only APIs (unavailable over HTTP — Portainer is commonly deployed without TLS). // Each entry names the safe alternative. The approved implementations (e.g. useCopy) carry an // eslint-disable comment so this list stays the single source of truth. // // To check whether a new API requires a secure context, fetch its MDN page and look for // "Secure context" in the response: // https://developer.mozilla.org/en-US/docs/Web/API/{Interface}/{method} // See docs/guidelines/frontend-conventions.md § "Secure-context APIs" for the full policy. 'no-restricted-properties': [ 'error', { object: 'crypto', property: 'randomUUID', message: "crypto.randomUUID() requires a secure context (HTTPS). Use `import { v4 as uuidv4 } from 'uuid'` instead.", }, { object: 'crypto', property: 'subtle', message: 'crypto.subtle requires a secure context (HTTPS). Use a server-side cryptographic alternative.', }, { object: 'navigator', property: 'clipboard', message: 'navigator.clipboard requires a secure context (HTTPS). Use the `useCopy` hook or `CopyButton` component (@@/buttons/CopyButton) — they include a non-secure fallback.', }, { object: 'navigator', property: 'mediaDevices', message: 'navigator.mediaDevices requires a secure context (HTTPS) and will not work in all Portainer deployment contexts.', }, { object: 'navigator', property: 'credentials', message: 'navigator.credentials requires a secure context (HTTPS) and will not work in all Portainer deployment contexts.', }, { object: 'navigator', property: 'serviceWorker', message: 'navigator.serviceWorker requires a secure context (HTTPS) and will not work in all Portainer deployment contexts.', }, ], '@typescript-eslint/no-use-before-define': ['error', { functions: false, allowNamedExports: true }], '@typescript-eslint/no-shadow': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', // TODO: fix violations and promote to 'error' '@typescript-eslint/no-unused-vars': 'warn', '@typescript-eslint/no-explicit-any': 'error', // TODO: fix violations and promote to 'error' '@typescript-eslint/switch-exhaustiveness-check': 'warn', '@typescript-eslint/no-restricted-imports': [ 'error', { patterns: [ { group: ['@/react/test-utils/*'], message: 'These utils are just for test files', }, ], }, ], 'import/first': 'error', 'import/newline-after-import': 'error', 'import/prefer-default-export': 'off', 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], 'import/order': [ 'error', { pathGroups: [ { pattern: '@api/**', group: 'internal', position: 'before' }, { pattern: '@@/**', group: 'internal', position: 'after' }, { pattern: '@/**', group: 'internal' }, ], groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 'newlines-between': 'always', }, ], 'react/no-unused-prop-types': 'error', 'react/button-has-type': 'error', 'react/forbid-prop-types': 'off', 'react/require-default-props': 'off', 'react/jsx-filename-extension': 'off', 'react/jsx-no-bind': 'off', 'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }], 'react/function-component-definition': ['error', { namedComponents: 'function-declaration' }], // Enabled explicitly (was brought in by airbnb, turned off in HOC/test/story files below) 'react/jsx-props-no-spreading': 'error', 'react/destructuring-assignment': ['error', 'always'], // dangerouslySetInnerHTML is allowed when the value is sanitized — add a targeted // suppress with a comment explaining the sanitization at each call site 'react/no-danger': 'error', // TODO: fix the underlying violations and promote these to 'error' 'react-hooks/refs': 'warn', 'react-hooks/set-state-in-effect': 'warn', 'react-hooks/immutability': 'warn', 'react-hooks/static-components': 'warn', 'react-hooks/purity': 'warn', 'react-hooks/globals': 'warn', 'react-hooks/preserve-manual-memoization': 'warn', 'jsx-a11y/no-autofocus': 'warn', 'jsx-a11y/label-has-associated-control': ['error', { assert: 'either', controlComponents: ['Input', 'Checkbox'] }], 'jsx-a11y/control-has-associated-label': 'off', 'regex/invalid': [ 'error', [ { regex: '