eslint-plugin-vue icon indicating copy to clipboard operation
eslint-plugin-vue copied to clipboard

Full example for flat config with object

Open Narretz opened this issue 10 months ago • 2 comments

Tell us about your environment

  • ESLint version: 9
  • eslint-plugin-vue version: 9
  • Vue version: 2
  • Node version: 20

The problem you want to solve. I converted my eslint config to flat config. I used a lot of overrides, and tried to recreated that with specific objects that contain plugins etc.

So for eample previously my vue config looked like this:

overrides: [
    {
      files: ['packages/client/src/components/**/*.vue', 'stories/**/*.vue'],
      extends: ['plugin:vue/strongly-recommended'],
      parser: 'vue-eslint-parser',
      parserOptions: {
        parser: '@typescript-eslint/parser',
      },
      rules: {
        'max-lines': ['error', { max: 900 }],
      }
    }
]

The only way I managed to recreate this is by exactly spreading the arrays in the flat vue config. This involved looking at the plugin to see how many arrays it includes. Is there no easier way? I don't even know if I have included everything.

  {
    files: ['packages/client/src/components/**/*.vue', 'stories/**/*.vue'],
    languageOptions: {
      ...vue.configs['flat/vue2-strongly-recommended'][1].languageOptions,
      parserOptions: {
        parser: '@typescript-eslint/parser',
      },
    },
    plugins: {
      vue,
    },
    rules: {
      ...vue.configs['flat/vue2-strongly-recommended'][2].rules,
      ...vue.configs['flat/vue2-strongly-recommended'][3].rules,
      'max-lines': ['error', { max: 900 }],
    },
  },

Your take on the correct solution to problem.

I don't have a good idea, but it would definitely help if the rules and the language options were accessible in a merged object.

Narretz avatar Apr 14 '24 11:04 Narretz

This is an example repo I use for performance testing, but this is the general approach I use in my repos: https://github.com/higherorderfunctor/effect/blob/chore/eslint-testing/eslint.config.js

I took the inverse approach of copying the common stuff into each plugins rule sets with a mapping function instead of trying to copy rules into a single object with the common stuff.

const overrideWith = (overrides, configs) =>
  configs.map((config) =>
    { ...config, ...overrides, plugins: { ...overrides.plugins, ...(config.plugins ?? {}) } }
  )

const tsOverrides = {
  ... // common settings
}

const tsRules = overrideWith(tsOverrides, [
  eslint.configs.recommended, // no array
  tseslint.configs.eslintRecommended, // no array
  ...tseslint.configs.strictTypeChecked, // array
  { rules: effectPlugin.configs.recommended.rules }, // only rules
  {
  ...
  }
])

const vueOverrides = {
  ... // common settings
}

const vueRules = overrideWith(vueOverrides, [
  ...tsRules,
  ...vuePlugin.configs["flat/recommended"], // array
  ...compat.extends("@vue/eslint-config-typescript/recommended"), // compat to array
  ...compat.extends("plugin:vuetify/recommended"), // compat to array
  ...vueA11yPlugin.configs["flat/recommended"], // array
  ...compat.extends("@vue/eslint-config-prettier"), // array
  { rules: { "codegen/codegen": "off" } } // doesn't work with vue
])

const eslintConfig = [
  // global configs
  {
    ignores: ["dist", "build", "docs", "*.md"],
    linterOptions: {
      reportUnusedDisableDirectives: true
    }
  },
  // filetype configs
  ...tsRules,
  ...vueRules
]

export default eslintConfig

higherorderfunctor avatar Jun 18 '24 00:06 higherorderfunctor

UPDATE:

Ignore all the rambling below. The "proper" way to solve this is to rely on ESLint itself to merge the multiple-definitions for you.

Documented here - https://eslint.org/docs/latest/use/configure/combine-configs

The way to actually use it in your example,

[
  ...vue.configs['flat/vue2-strongly-recommended'].map(cfg => {
    cfg.files = ['packages/client/src/components/**/*.vue', 'stories/**/*.vue']
    return cfg
  },
  {
    files: ['packages/client/src/components/**/*.vue', 'stories/**/*.vue'],
    languageOptions: {
      parserOptions: {
        parser: '@typescript-eslint/parser',
      },
    },
    plugins: { vue },
    rules: {
      'max-lines': ['error', { max: 900 }],
    },
  }
]

This way, first you get the stack of recommended configurations, while only changing the files property (that they don't have) by overwriting it. And then applying your own configuration on the same files, relying on the fact that you already have all the recommended rules in place.

Just be careful to keep these objects separate and sequential in the correct order.


IGNORE THIS:

I went into the rabbit hole of deep merging to "properly" solve this.

Your code should merge the sequence of the recommended configurations, by adding them together, not overwriting each other's properties, and then adding your own. So in order to get a working result, you would want something like this -

import deepmerge from 'deepmerge'

deepmerge.all([
  ...vue.configs['flat/vue2-strongly-recommended'],
  {
    files: ['packages/client/src/components/**/*.vue', 'stories/**/*.vue'],
    languageOptions: {
      parserOptions: {
        parser: '@typescript-eslint/parser',
      },
    },
    plugins: { vue },
    rules: {
      'max-lines': ['error', { max: 900 }],
    },
  }
])

But of course life is not so simple, and circular references, and non-plain-objects are going to be located everywhere in these plugins. So a better way to merge would be this -

import deepmerge from 'deepmerge'
import { isPlainObject } from 'is-plain-object'

deepmerge.all([
  ...vue.configs['flat/vue2-strongly-recommended'],
  {
    files: ['packages/client/src/components/**/*.vue', 'stories/**/*.vue'],
    // ...
  }
], { isMergeableObject: isPlainObject)

But then you will always find some "special" plugins that will just insist on having real weird objects in their configuration, so you end up with a monstrosity like this -

import deepmerge from 'deepmerge'
import { isPlainObject } from 'is-plain-object'

const merge = (...cfgs: ConfigWithExtends[]) => {
  const objCache = new Set()
  return deepmerge.all(cfgs, {
    isMergeableObject: (o: Record<string, any>) => {
      if (typeof o === 'object') {
        if (objCache.has(o))
          return false
        objCache.add(o)
        for (const key in o)
          if (o[key] === null)
            delete o[key]
        return true
      }
      return isPlainObject(o)
    }
  })
}

and you use it like this --

merge([
  ...vue.configs['flat/vue2-strongly-recommended'],
  {
    files: ['packages/client/src/components/**/*.vue', 'stories/**/*.vue'],
    // ...
  }
]

And for those who think you can just ...splat objects in JS and get a combined deeply-merged object. You should test with an example like so -

{
  ...{ rules: { a: "aaa" } },
  ...{ rules: { b: "bbb" } },
}

One would expect to see something like { rules: { a: "aaa", b: "bbb" } } as a result? Right? Wrong! You will get the last one only, since you overwrote the previous rules with the new rules. The result is { rules: { b: "bbb" } }

kesor avatar Jun 18 '24 02:06 kesor