Eslint configuration for Meteor 3, Eslint, TypeScript and WebStorm

I just set-up a new Meteor project using meteor create using the typescript setup and wanted to add the current eslint to it. I’d also like to use it for auto-formatting in WebStorm.

However, the docs in the Meteor guide are a bit outdated, they only describe how to include the @meteorjs/eslint-config-meteor which hasn’t been updated for a while.

What is the best way to get a working eslint 9 configuration that supports Meteor, Mocha, TypeScript, and React?

Do you have an eslint configuration for Eslint 8 such as an .eslintrc file?

Yes, but this stems from a Meteor project that was not using TypeScript. I tried to convert it using the automatic converter, but this failed.

module.exports = {
    parser: 'babel-eslint',
    parserOptions: {
        sourceType: 'module',
        ecmaVersion: 2019
    },
    plugins: ['babel', 'mocha'],
    extends: [
        'standard',
        'standard-react',
        'plugin:import/errors',
        'plugin:import/warnings',
        'plugin:mocha/recommended'
    ],
    env: {
        node: true,
        browser: true,
        mocha: true
    },
    rules: {
        'babel/semi': 0,
        'comma-dangle': 0,
        'jsx-quotes': ['error', 'prefer-double'],
        'no-return-assign': 0,
        'no-trailing-spaces': 0,
        'react/react-in-jsx-scope': 0,
        semi: [2, 'always'],
        'react/prop-types': 0,
        'react/jsx-tag-spacing': 0,
        'space-before-function-paren': 0,
        indent: ['error', 2, {SwitchCase: 1, ignoredNodes: ['ConditionalExpression *']}],
        eqeqeq: ['error', 'always']
    },
    settings: {
        'import/resolver': {
            meteor: {
                extensions: ['.js', '.jsx']
            }
        }
    }
};

I also tried to setup this old file using the old eslint to auto-format TypeScript files. But this also failed. I ran into a “missing module eslint/use-at-your-own-risk” error. Seems as something is not compatible.

I don’t care if it’s actually eslint 9, could also work with 8 or older, as long as TypeScript auto-formatting on save works in WebStorm.

Did you use this?

meteor npm install @eslint/compat globals @eslint/js @eslint/eslintrc -D --force
meteor npm install --save-dev eslint-config-standard eslint-plugin-promise eslint-plugin-import eslint-plugin-n --force

npx @eslint/migrate-config .eslintrc

from here: Migrate to ESLint 9.x. In this article, I share some… | by Duy NG | ekino-france | Medium
This is what I used yesterday in order to migrate from 8 t 9

I used that npx, but not the rest. Will try it.

But as stated above, I do not actually care about eslint 9 vs. 8. All I need is a working eslint setup that supports Meteor 3 and auto-format on save for TypeScript in WebStorm.

I just returned to my old config, and it kinda works even with TypeScript. But I get a couple of errors, e.g. here:

Also, keywords like as are flagged as an error. So there seems to be something wrong with the TypeScript setup (I’m just using the bare-bone Meteor scaffold right now).

I see you use standard for JS. I believe there is also a ts-standard for TS. You also need to make sure Webstorm has TS enabled (I think by default is enabled, I had to turned it of before performance issues with Material UI) and beware of Webstorm + TS issues such as: https://youtrack.jetbrains.com/issue/WEB-52943/Meta-High-CPU-usage-on-resolve-or-types-evaluation-in-TypeScript

1 Like

Thanks. I now got it working with eslint 8. It’s really crazy how long these configs take to adapt :slight_smile:

The only thing I did not get working is the resolver for module names like meteor/meteor. Seems as if the Meteor module resolver is not compatible with the TypeScript parser.

Are you using https://typescript-eslint.io/ ?

I’m using the parser right now, but not the new configuration file setup.

Bump. I’m still looking for a working ESLint 9 configuration as a base for Meteor 3 that also takes care of Meteor’s special import resolver, so imports like meteor/meteor are correctly resolved. I got it working with ESLint 8 now, but not for 9.

Here’s my config for ESLint 8, in case anybody is interested:

module.exports = {
  root: true,
  parser: '@typescript-eslint/parser',
  env: {
    node: true,
    browser: true
  },
  plugins: ['@typescript-eslint'],
  extends: [
    'eslint:recommended',
    'standard-react',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/stylistic'
  ],
  settings: {
    'import/resolver': {
      meteor: {
        extensions: ['.js', '.jsx', '.ts', '.tsx']
      },
      moduleDirectory: ['node_modules']
    }
  },
  rules: {
    'arrow-spacing': ['error', { before: true, after: true }],
    'comma-dangle': ['error', 'never'],
    'comma-style': ['error', 'last'],
    'eol-last': ['error', 'always'],
    eqeqeq: ['error', 'always'],
    indent: ['error', 2, { SwitchCase: 1, ignoredNodes: ['ConditionalExpression *'] }],
    'jsx-quotes': ['error', 'prefer-double'],
    'key-spacing': ['error', { beforeColon: false }],
    'keyword-spacing': ['error'],
    'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0 }],
    'no-multi-spaces': 'error',
    'no-return-assign': 0,
    'no-trailing-spaces': 'error',
    'object-curly-spacing': ['error', 'always'],
    quotes: ['error', 'single'],
    'quote-props': ['error', 'as-needed'],
    'react/jsx-tag-spacing': ['error', {
      closingSlash: 'never',
      beforeSelfClosing: 'always',
      afterOpening: 'never',
      beforeClosing: 'allow'
    }],
    'react/prop-types': 0,
    'react/react-in-jsx-scope': 0,
    semi: [2, 'always'],
    'semi-spacing': ['error', { before: false, after: true }],
    'space-before-function-paren': 0,
    'space-in-parens': ['error', 'never'],
    'space-infix-ops': ['error', { int32Hint: false }]
  }
};

I don’t use TS but you could probably start from this. I have this in the root of the project in a file named eslint.config.mjs

import react from 'eslint-plugin-react'
import n from 'eslint-plugin-n'
import meteor from 'eslint-plugin-meteor'
import reactHooks from 'eslint-plugin-react-hooks'
import security from 'eslint-plugin-security'
import babel from '@babel/eslint-plugin'
import customRules from 'eslint-plugin-custom-rules'
import meteorError from 'eslint-plugin-meteor-error'
import { fixupPluginRules } from '@eslint/compat'
import globals from 'globals'
import babelParser from '@babel/eslint-parser'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import js from '@eslint/js'
import { FlatCompat } from '@eslint/eslintrc'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const compat = new FlatCompat({
  baseDirectory: __dirname,
  recommendedConfig: js.configs.recommended,
  allConfig: js.configs.all
})

export default [...compat.extends(
  'standard',
  'standard-jsx',
  'eslint:recommended',
  'plugin:n/recommended',
  'plugin:react/recommended',
  'plugin:meteor/recommended'
), {
  plugins: {
    react,
    n,
    meteor,
    'react-hooks': fixupPluginRules(reactHooks),
    security,
    '@babel': babel,
    'custom-rules': customRules,
    'meteor-error': meteorError
  },

  languageOptions: {
    globals: {
      ...globals.meteor,
      ...globals.browser,
      ...globals.node,
      Atomics: 'readonly',
      SharedArrayBuffer: 'readonly',
      Meteor: false,
      Session: false
    },

    parser: babelParser,
    ecmaVersion: 13,
    sourceType: 'module',

    parserOptions: {
      allowImportExportEverywhere: true,

      ecmaFeatures: {
        jsx: true
      }
    }
  },

  settings: {
    'import/resolver': 'meteor',

    react: {
      createClass: 'createReactClass',
      pragma: 'React',
      version: 'detect',
      flowVersion: '0.53'
    },

    propWrapperFunctions: ['forbidExtraProps', {
      property: 'freeze',
      object: 'Object'
    }, {
      property: 'myFavoriteWrapper'
    }],

    linkComponents: ['Hyperlink', {
      name: 'Link',
      linkAttribute: 'to'
    }]
  },

  rules: {
    'no-new': 'off',
    'no-console': 'off',
    'no-undef': [2],
    'no-case-declarations': 'off',
    'react/jsx-first-prop-new-line': 'off',
    'react/jsx-wrap-multilines': 'off',
    'react/jsx-closing-tag-location': 'off',
    'react/jsx-indent': 'off',
    'object-curly-newline': 'off',
    'react/prop-types': 'off',
    'react/react-in-jsx-scope': 'off',
    'react/jsx-uses-react': 'off',
    'n/no-unsupported-features/es-syntax': 'off',
    'n/no-missing-import': 'off',
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn',
    'import/no-extraneous-dependencies': 0,
    'meteor/no-session': 0,
    'meteor/audit-argument-checks': 0,
    'n/no-unsupported-features/node-builtins': 'off',
    'no-restricted-imports': ['error', {
      patterns: ['@mui/*/*/*', '!@mui/material/test-utils/*']
    }],

    'meteor-error/transform-error-constructor': 2
  }
}]

1 Like