mastodon/eslint.config.mjs
Renaud Chaput 2d9395ecf8
Experimentation with switching to Tanstack Router
This is not really working except for 3 screens (Home, Notifications, Explore).

Multi-columns management and all the login from `<UI>` has not been implemented yet.

Many components are still using `<Link>` or `withRouter` and will fail if renderer inside the new router

Some links have been converted to plain links and will need to be switched back

There are issues with loading content, because this is tied to the previous scroll restoration and is not triggering properly. All the data loading should probably be reworked completely to fit with the new router
2025-08-17 11:04:00 +02:00

438 lines
11 KiB
JavaScript

// @ts-check
import path from 'node:path';
import js from '@eslint/js';
import { globalIgnores } from 'eslint/config';
import formatjs from 'eslint-plugin-formatjs';
import importPlugin from 'eslint-plugin-import';
import jsdoc from 'eslint-plugin-jsdoc';
import jsxA11Y from 'eslint-plugin-jsx-a11y';
import promisePlugin from 'eslint-plugin-promise';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import storybook from 'eslint-plugin-storybook';
import globals from 'globals';
import tseslint from 'typescript-eslint';
/** @type {import('typescript-eslint').ConfigArray} */
export const baseConfig = [
js.configs.recommended,
importPlugin.flatConfigs.recommended,
jsdoc.configs['flat/recommended'],
promisePlugin.configs['flat/recommended'],
{
linterOptions: {
reportUnusedDisableDirectives: 'error',
reportUnusedInlineConfigs: 'error',
},
rules: {
'consistent-return': 'error',
'dot-notation': 'error',
eqeqeq: [
'error',
'always',
{
null: 'ignore',
},
],
'no-console': [
'warn',
{
allow: ['error', 'warn'],
},
],
'no-empty': [
'error',
{
allowEmptyCatch: true,
},
],
'no-restricted-properties': [
'error',
{
property: 'substring',
message: 'Use .slice instead of .substring.',
},
{
property: 'substr',
message: 'Use .slice instead of .substr.',
},
],
'no-unused-expressions': 'error',
'no-unused-vars': 'off',
'valid-typeof': 'error',
'import/extensions': [
'error',
'always',
{
js: 'never',
jsx: 'never',
mjs: 'never',
ts: 'never',
mts: 'never',
tsx: 'never',
},
],
'import/first': 'error',
'import/newline-after-import': 'error',
'import/no-anonymous-default-export': 'error',
'import/no-amd': 'error',
'import/no-commonjs': 'error',
'import/no-import-module-exports': 'error',
'import/no-relative-packages': 'error',
'import/no-self-import': 'error',
'import/no-useless-path-segments': 'error',
'import/order': [
'error',
{
alphabetize: {
order: 'asc',
},
'newlines-between': 'always',
groups: [
'builtin',
'external',
'internal',
'parent',
['index', 'sibling'],
'object',
],
pathGroups: [
{
pattern: '{react,react-dom,react-dom/client,prop-types}',
group: 'builtin',
position: 'after',
},
{
pattern: '{react-intl,intl-messageformat}',
group: 'builtin',
position: 'after',
},
{
pattern:
'{classnames,react-helmet,react-router,react-router-dom}',
group: 'external',
position: 'before',
},
{
pattern:
'{immutable,@reduxjs/toolkit,react-redux,react-immutable-proptypes,react-immutable-pure-component}',
group: 'external',
position: 'before',
},
{
pattern: '{mastodon/**}',
group: 'internal',
position: 'after',
},
],
pathGroupsExcludedImportTypes: [],
},
],
'jsdoc/check-types': 'off',
'jsdoc/no-undefined-types': 'off',
'jsdoc/require-jsdoc': 'off',
'jsdoc/require-param-description': 'off',
'jsdoc/require-property-description': 'off',
'jsdoc/require-returns-description': 'off',
'jsdoc/require-returns': 'off',
'promise/always-return': 'off',
'promise/catch-or-return': [
'error',
{
allowFinally: true,
},
],
'promise/no-callback-in-promise': 'off',
'promise/no-nesting': 'off',
'promise/no-promise-in-callback': 'off',
},
},
];
export default tseslint.config([
baseConfig,
globalIgnores([
'build/**/*',
'coverage/**/*',
'db/**/*',
'lib/**/*',
'log/**/*',
'node_modules/**/*',
'public/**/*',
'!public/embed.js',
'spec/**/*',
'tmp/**/*',
'vendor/**/*',
'streaming/**/*',
]),
react.configs.flat.recommended,
react.configs.flat['jsx-runtime'],
reactHooks.configs['recommended-latest'],
jsxA11Y.flatConfigs.recommended,
importPlugin.flatConfigs.react,
// @ts-expect-error -- For some reason the formatjs package exports an empty object?
formatjs.configs.strict,
storybook.configs['flat/recommended'],
{
languageOptions: {
globals: {
...globals.browser,
},
parser: tseslint.parser,
ecmaVersion: 2021,
sourceType: 'module',
},
settings: {
react: {
version: 'detect',
},
'import/ignore': ['node_modules', '\\.(css|scss|json)$'],
'import/resolver': {
typescript: {
project: path.resolve(import.meta.dirname, './tsconfig.json'),
},
},
},
rules: {
'no-restricted-syntax': [
'error',
{
// eslint-disable-next-line no-restricted-syntax
selector: 'Literal[value=/•/], JSXText[value=/•/]',
// eslint-disable-next-line no-restricted-syntax
message: "Use '·' (middle dot) instead of '•' (bullet)",
},
],
'formatjs/enforce-description': 'off', // description values not currently used
'formatjs/enforce-id': 'off', // Explicit IDs are used in the project
'formatjs/enforce-placeholders': 'off', // Issues in short_number.jsx
'formatjs/no-invalid-icu': 'error',
'formatjs/no-literal-string-in-jsx': 'off', // Should be looked at, but mainly flagging punctuation outside of strings
'formatjs/no-multiple-plurals': 'off', // Should be looked at
'jsx-a11y/click-events-have-key-events': 'off',
'jsx-a11y/label-has-associated-control': 'off',
'jsx-a11y/media-has-caption': 'off',
'jsx-a11y/no-autofocus': 'off',
'jsx-a11y/no-interactive-element-to-noninteractive-role': 'off',
'jsx-a11y/no-noninteractive-tabindex': 'off',
'jsx-a11y/no-static-element-interactions': [
'warn',
{
handlers: ['onClick'],
},
],
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: [
'eslint.config.mjs',
'app/javascript/mastodon/performance.js',
'app/javascript/testing/**/*',
'app/javascript/**/__tests__/**',
'app/javascript/**/*.stories.ts',
'app/javascript/**/*.stories.tsx',
'app/javascript/**/*.test.ts',
'app/javascript/**/*.test.tsx',
'.storybook/**/*',
],
},
],
'import/no-unresolved': [
'error',
{
ignore: ['vite/modulepreload-polyfill', '^virtual:.+'],
},
],
'react/jsx-filename-extension': [
'error',
{
extensions: ['.jsx', 'tsx'],
},
],
'react/jsx-boolean-value': 'error',
'react/display-name': 'off',
'react/jsx-fragments': ['error', 'syntax'],
'react/jsx-equals-spacing': 'error',
'react/jsx-no-bind': 'error',
'react/jsx-no-useless-fragment': 'error',
'react/jsx-no-target-blank': [
'error',
{
allowReferrer: true,
},
],
'react/jsx-tag-spacing': 'error',
'react/jsx-wrap-multilines': 'error',
'react/self-closing-comp': 'error',
},
},
{
files: [
'app/javascript/mastodon/common.js',
'app/javascript/mastodon/features/emoji/unicode_to_unified_name.js',
'app/javascript/mastodon/features/emoji/emoji_compressed.js',
'app/javascript/mastodon/features/emoji/unicode_to_filename.js',
'app/javascript/mastodon/service_worker/web_push_locales.js',
'**/*.config.js',
'**/.*rc.js',
'**/ide-helper.js',
'config/formatjs-formatter.js',
],
languageOptions: {
globals: {
...globals.commonjs,
...globals.node,
},
ecmaVersion: 5,
sourceType: 'commonjs',
},
rules: {
'import/no-commonjs': 'off',
},
},
{
files: ['**/*.ts', '**/*.tsx'],
extends: [
tseslint.configs.strictTypeChecked,
tseslint.configs.stylisticTypeChecked,
react.configs.flat.recommended,
react.configs.flat['jsx-runtime'],
reactHooks.configs['recommended-latest'],
jsxA11Y.flatConfigs.recommended,
importPlugin.flatConfigs.react,
importPlugin.flatConfigs.typescript,
jsdoc.configs['flat/recommended-typescript'],
],
languageOptions: {
parserOptions: {
projectService: true,
},
},
rules: {
// This is not needed as we use noImplicitReturns, which handles this in addition to understanding types
'consistent-return': 'off',
'formatjs/enforce-plural-rules': 'off',
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
'import/no-default-export': 'warn',
'jsdoc/require-jsdoc': 'off',
'jsdoc/require-param': 'off',
'jsdoc/require-returns': 'off',
'react/prefer-stateless-function': 'warn',
'react/function-component-definition': [
'error',
{
namedComponents: 'arrow-function',
},
],
'react/prop-types': 'off',
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
'@typescript-eslint/consistent-type-exports': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/prefer-nullish-coalescing': [
'error',
{
ignorePrimitives: {
boolean: true,
},
},
],
'@typescript-eslint/no-restricted-imports': [
'warn',
{
name: 'react-redux',
importNames: ['useSelector', 'useDispatch'],
message:
'Use typed hooks `useAppDispatch` and `useAppSelector` instead.',
},
],
'@typescript-eslint/no-unused-vars': [
'error',
{
vars: 'all',
args: 'after-used',
destructuredArrayIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
'@typescript-eslint/restrict-template-expressions': [
'warn',
{
allowNumber: true,
},
],
},
},
{
files: ['app/javascript/mastodon/routes/**/*.tsx'],
rules: {
'@typescript-eslint/no-use-before-define': [
'warn',
{ allowNamedExports: true, variables: false },
],
},
},
{
files: ['**/__tests__/*.js', '**/__tests__/*.jsx'],
languageOptions: {
globals: globals.vitest,
},
},
{
files: ['**/*.test.*'],
rules: {
'no-global-assign': 'off',
},
},
{
files: ['**/*.stories.ts', '**/*.stories.tsx', '.storybook/*'],
rules: {
'import/no-default-export': 'off',
},
},
{
files: ['vitest.shims.d.ts'],
rules: {
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off',
'@typescript-eslint/no-unnecessary-condition': 'off',
'@typescript-eslint/prefer-nullish-coalescing': 'off',
},
},
]);